sanity-image 0.1.0 → 0.1.2

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/README.md ADDED
@@ -0,0 +1,384 @@
1
+ # sanity-image
2
+
3
+ [![Latest version](https://img.shields.io/npm/v/sanity-image?label=version&color=brightGreen&logo=npm)](https://www.npmjs.com/package/sanity-image)
4
+ ![Dependency status](https://img.shields.io/librariesio/release/npm/sanity-image)
5
+ [![Open issues](https://img.shields.io/github/issues/coreyward/sanity-image)](https://github.com/coreyward/sanity-image/issues)
6
+ ![React version compatibility](https://img.shields.io/badge/dynamic/json?color=blue&label=react%20versions&query=peerDependencies.react&url=https%3A%2F%2Fraw.githubusercontent.com%2Fcoreyward%2Fsanity-image%2Fmain%2Fpackage.json)
7
+
8
+ A well-considered React component for displaying images from Sanity. At a
9
+ glance:
10
+
11
+ - Outputs a single `<img>` tag, no nested DOM structure to mess with
12
+ - Zero styling included so you can style it however you want…it's just an `img`
13
+ tag!
14
+ - Supports low-quality image previews out of the box, without build-time
15
+ penalties (native lazy loading)
16
+ - Generates a `srcSet` automatically based on the `width` you specify
17
+ - Dynamic `srcSet` factor based on image output width
18
+ - Knows _exactly_ what size the image will be and sets `width` and `height`
19
+ attributes accordingly
20
+ - Supports `crop` and `hotspot` values from the Sanity Studio
21
+ - Automatically crops to the most “interesting” part of the image if the aspect
22
+ ratio changes and no `hotspot` is provided
23
+ - Images are _never_ scaled up
24
+ - Tiny 4kb bundle size (2kb gzipped)
25
+ - No dependencies
26
+ - TypeScript support
27
+ - Works with Gatsby, Next.js, and any other React-based framework
28
+ - Polymorphic component (supports `as` prop to render as a custom component)
29
+
30
+ ## Quick Start
31
+
32
+ ### Install it:
33
+
34
+ ```sh
35
+ yarn add sanity-image
36
+ # or
37
+ npm install sanity-image
38
+ ```
39
+
40
+ ### Use it:
41
+
42
+ You can find the full writeup on getting going below, but in the interest of
43
+ making it easy to see if this is the thing you are looking for, here’s a quick
44
+ example of most of what you’ll need to know:
45
+
46
+ **Simplest Case**:
47
+
48
+ This will render the image out assuming it will be displayed at half its
49
+ original width with a srcSet included (multiplies vary based on original image
50
+ size):
51
+
52
+ ```tsx
53
+ import { SanityImage } from "sanity-image"
54
+
55
+ const YourSweetComponent = ({ image }: ComponentProps) => (
56
+ <Image
57
+ // Pass the Sanity Image ID (`_id`) (e.g., `image-abcde12345-1200x800-jpg`)
58
+ id={image._id}
59
+ baseUrl="https://cdn.sanity.io/images/abcd1234/production"
60
+ alt="Demo image"
61
+ />
62
+ )
63
+ ```
64
+
65
+ **More full-featured example**:
66
+
67
+ ```tsx
68
+ import { SanityImage } from "sanity-image"
69
+
70
+ const YourSweetComponent = ({ image }: ComponentProps) => (
71
+ <Image
72
+ // Pass the Sanity Image ID (`_id`) (e.g., `image-abcde12345-1200x800-jpg`)
73
+ id={image._id}
74
+ //
75
+ // You can set the base URL manually, or let it be constructed by passing
76
+ // `projectId` and `dataset` props.
77
+ baseUrl="https://cdn.sanity.io/images/abcd1234/production"
78
+ //
79
+ // Specify how big it is expected to render so a reasonable srcSet can be
80
+ // generated using `width`, `height`, or both
81
+ width={500}
82
+ height={250}
83
+ //
84
+ // Choose whether you want it to act like `object-fit: cover` or
85
+ // `object-fit: contain`, or leave it out to use the default (contain)
86
+ mode="cover"
87
+ //
88
+ // Have hotspot or crop data from Sanity? Pass it in!
89
+ hotspot={image.hotspot}
90
+ crop={image.crop}
91
+ //
92
+ // Want low-quality image previews? Fetch them from Sanity and pass them in too.
93
+ preview={image.asset.metadata.lqip}
94
+ //
95
+ // Have a burning desire to have Sanity change the format or something?
96
+ // Most of the visual effects from the Sanity Image API are available:
97
+ queryParams={{ sharpen: 30, q: 80 }}
98
+ //
99
+ // Anything else you want to pass through to the img tag? Go for it!
100
+ alt="Sweet Christmas!"
101
+ className="big-ol-image"
102
+ sizes="(min-width: 500px) 500px, 100vw"
103
+ />
104
+ )
105
+
106
+ export default YourSweetComponent
107
+ ```
108
+
109
+ That’s the gist. Read on for more. 👇
110
+
111
+ ## Details
112
+
113
+ How it works at a glance:
114
+
115
+ - The image ID is parsed to determine the source image dimensions and format
116
+ - SVG images get special treatment from the Sanity Image API (they don't support
117
+ params), so they're handled a bit differently (check `SanityImage.ts` for
118
+ details)
119
+ - All other images have `src` and `srcSet` props generated based on the `width`
120
+ and `height` props you pass in (or the image dimensions if you don't pass in a
121
+ width or height)
122
+ - The `srcSet` widths depend on the size of the output image and the original
123
+ image; there's some logic to avoid wasteful tiny images or giant jumps in size
124
+ between large entries (see `dynamicMultipliers` in `urlBuilder.ts`)
125
+ - Values in the `srcSet` are never duplicated and never upscale the image
126
+ - Since we can compute the output dimensions of the image in all cases, the
127
+ `width` and `height` attributes are set automatically to avoid layout shifts
128
+ - A few image params are applied by default:
129
+ - `auto=format` - Sanity will use WebP images if they're supported by the
130
+ browser (note: if you specify `fm` manually, this won't be set)
131
+ - `fit` - if the image aspect ratio isn't changed, this will be set to `max`;
132
+ if the aspect ratio will change it's set to `crop`; you don't really need to
133
+ worry about this though
134
+ - `q` - the quality is set to 75 by default, but you can override it with the
135
+ `queryParams` prop
136
+ - The `loading` attribute will be set to `lazy` if it isn't supplied; use
137
+ `loading="eager"` for images above the fold
138
+ - The `alt` attribute will be set to an empty string if it isn't supplied; set
139
+ it if it isn't a decorative image!
140
+ - By default it renders an `img` tag (two if you pass in a `preview`), but you
141
+ can pass in a custom component to render as using the `as` prop (see the
142
+ `SanityImage.test.tsx` file for an example of this)
143
+ - If you wanna get weird you can also import the `buildSrc` and `buildSrcSet`
144
+ exports to do your own thing with. You get a lot of the magic still this way
145
+ with a skosh more control.
146
+ - Similarly, the `parseImageId` function is available as a named export; it
147
+ takes an image ID and returns an object with the image id, dimensions, and
148
+ format.
149
+ - Query params passed to Sanity are all sorted and minimized like heck for
150
+ improved caching and smaller URLs. Pass in a `height` only? Don't be alarmed,
151
+ but it'll be converted to a `w` param without altering what you're asking
152
+ Sanity for. Ask for `mode="cover"` but the aspect ratio matches the source?
153
+ It'll be ignored and fall back to `fit=max` with just a `w` param. You get the
154
+ idea (I hope, or at least, I'm pretending, but no judgement if you don't, it's
155
+ definitely 11:09pm and I'm on fumes)
156
+
157
+ ## Props
158
+
159
+ This is mostly copied and reformatted from the `types.ts` file; if you're
160
+ comfortable with TypeScript, that might give you more detail.
161
+
162
+ - `id` (string) — Required - The Sanity Image ID (`_id` or `_ref` field value)
163
+ - `mode` ("cover" | "contain") — Optional - Use `cover` to crop the image to
164
+ match the requested aspect ratio (based on `width` and `height`). Use
165
+ `contain` to fit the image to the boundaries provided without altering the
166
+ aspect ratio. Defaults to `"contain"`.
167
+ - `width` (number) — Optional - The target width of the image in pixels. Only
168
+ used for determining the dimensions of the generated assets, not for layout.
169
+ Use CSS to specify how the browser should render the image instead.
170
+ - `height` (number) — Optional - The target height of the image in pixels. Only
171
+ used for determining the dimensions of the generated assets, not for layout.
172
+ Use CSS to specify how the browser should render the image instead.
173
+ - `hotspot` (`{ x: number, y: number }`) — Optional - The hotspot coordinates to
174
+ use for the image. Note: hotspot `width` and `height` are not used.
175
+ - `crop` (`{ top: number, bottom: number, left: number, right: number }`) —
176
+ Optional - The crop coordinates to use for the image.
177
+ - `preview` (string) — Optional - A low-quality image preview to use while the
178
+ full-size image is loading. This should be a base64-encoded image string.
179
+ - `as` (React.ElementType) — Optional - The component to render as. Defaults to
180
+ `"img"`.
181
+ - `baseUrl` (string) — Optional - The base URL to use for the image. If not
182
+ specified, the `projectId` and `dataset` props will be used to construct the
183
+ URL.
184
+ - `projectId` (string) — Optional - The Sanity project ID to use for the image.
185
+ Only used if `baseUrl` is not specified.
186
+ - `dataset` (string) — Optional - The Sanity dataset to use for the image. Only
187
+ used if `baseUrl` is not specified.
188
+ - `queryParams` (object) — Optional - An object of query parameters to pass to
189
+ the Sanity Image API. See the
190
+ [Sanity Image API documentation](https://www.sanity.io/docs/image-urls) for a
191
+ list of available options.
192
+
193
+ That's the gist. There's a ton more in the inline comments and types and such,
194
+ and I'll add more details as I think of them. Feel free to open an issue or
195
+ start a discussion if you have questions or suggestions, or find me on the
196
+ Sanity Slack!
197
+
198
+ <details>
199
+ <summary><strong>⚠️ Minor gotchas with deferred loading</strong></summary>
200
+
201
+ `SanityImage` is relying on browser-native deferred image loading. This
202
+ generally works fine in browsers that support it, but there are situations where
203
+ the unloaded image is hidden or covered, resulting in the full image never
204
+ loading.
205
+
206
+ If this happens, you can override the styles set on the full-size image using
207
+ the `img[data-loading]` selector. This image sits immediately adjacent to the
208
+ spaceball image and has the following default styles _while loading_:
209
+
210
+ ```css
211
+ position: absolute;
212
+ width: 10px !important; /* must be > 4px to be lazy loaded */
213
+ height: 10px !important; /* must be > 4px to be lazy loaded */
214
+ opacity: 0;
215
+ zindex: -10;
216
+ pointerevents: none;
217
+ userselect: none;
218
+ ```
219
+
220
+ </details>
221
+
222
+ ## Tips
223
+
224
+ ### Wrap it internally
225
+
226
+ I recommend creating a wrapper component internally to pass your `baseUrl` prop
227
+ and pass through any props. This keeps the configuration in one place and gives
228
+ you an entry point to add any other logic you might need. Here's a TypeScript
229
+ example (for JavaScript, just remove the type annotation after `props`):
230
+
231
+ ```tsx
232
+ import { SanityImage } from "sanity-image"
233
+
234
+ const projectId = process.env.SANITY_PROJECT_ID
235
+ const dataset = process.env.SANITY_DATASET
236
+ const baseUrl = `https://cdn.sanity.io/images/${projectId}/${dataset}`
237
+
238
+ export const Image = (
239
+ props: Omit<
240
+ React.ComponentProps<typeof SanityImage>,
241
+ "baseUrl" | "dataset" | "projectId"
242
+ >
243
+ ) => <SanityImage baseUrl={baseUrl} {...props} />
244
+ ```
245
+
246
+ ### Styling your images
247
+
248
+ I recommend setting something like the following CSS for images in your project,
249
+ then overriding styles as needed. This will ensure images act like block-level
250
+ elements with infinitely scalable contents even with the `width` and `height`
251
+ attributes set. It also makes it easier to handle responsiveness—if your
252
+ container gets smaller, the image gets smaller.
253
+
254
+ ```css
255
+ img {
256
+ display: block;
257
+ max-width: 100%;
258
+ width: 100%;
259
+ height: auto;
260
+ }
261
+ ```
262
+
263
+ Here's an example of how that works when using, for example, a 3-column grid
264
+ that fills the viewport until it is a maximum of 1,200px wide (plus padding).
265
+ This produces columns that are 390px at most on desktop:
266
+
267
+ ```jsx
268
+ <div
269
+ css={{
270
+ display: "grid",
271
+ gridTemplateColumns: "repeat(3, 1fr)",
272
+ gap: 15,
273
+ maxWidth: 1240,
274
+ paddingInline: 20,
275
+ marginInline: "auto",
276
+ }}
277
+ >
278
+ {["image-a", "image-b", "image-c"].map((imageId) => (
279
+ <div key={imageId}>
280
+ <SanityImage
281
+ id={imageId}
282
+ baseUrl="..."
283
+ width={390}
284
+ sizes="(min-width: 1240px) 390px, calc((100vw - 40px - 30px) / 3)"
285
+ />
286
+ </div>
287
+ ))}
288
+ </div>
289
+ ```
290
+
291
+ If you need these images to all match in height, it's a good idea to switch to
292
+ `cover` mode. With the height set to 260px and `mode="cover"`, this will produce
293
+ images with a 3:2 aspect ratio that fill the column width even if the source
294
+ image is too small:
295
+
296
+ ```jsx
297
+ <SanityImage
298
+ id={imageId}
299
+ baseUrl="..."
300
+ width={390}
301
+ height={260}
302
+ mode="cover"
303
+ sizes="(min-width: 1240px) 390px, calc((100vw - 40px - 30px) / 3)"
304
+ />
305
+ ```
306
+
307
+ In this example we don't pass a `hotspot` value, so the image will be cropped
308
+ based on what Sanity thinks is the most interesting part of the image since
309
+ `SanityImage` automatically sets `crop=entropy` in these cases. If you want to
310
+ override that, you can pass a `hotspot` value.
311
+
312
+ ### Background images
313
+
314
+ Using `SanityImage` for background images is easy, you just style the image to
315
+ match the expectations of your mockup. In most cases that means setting
316
+ `position: relative` on the container you want to fill, then using absolute
317
+ positioning for the image. Here’s an example:
318
+
319
+ ```jsx
320
+ <section
321
+ css={{
322
+ position: "relative",
323
+ paddingBlock: 100,
324
+ }}
325
+ >
326
+ <SanityImage
327
+ id="..."
328
+ baseUrl="..."
329
+ width={1440}
330
+ css={{
331
+ position: "absolute",
332
+ top: 0,
333
+ left: 0,
334
+ width: "100%",
335
+ height: "100%",
336
+ objectFit: "cover",
337
+ userSelect: "none",
338
+ zIndex: 1,
339
+ }}
340
+ alt=""
341
+ />
342
+
343
+ <div css={{ position: "relative", zIndex: 2 }}>
344
+ <h1>Your big hero copy</h1>
345
+ <LinkButton to="/signup/">Get started</LinkButton>
346
+ </div>
347
+ </section>
348
+ ```
349
+
350
+ This will cause the `section` to be sized based on the content inside of the
351
+ `div`, and the image will be sized to fill the entire section. The aspect ratio
352
+ of the image will be maintained due to the use of `object-fit: cover`. Note that
353
+ we are still using `mode="contain"` for `SanityImage` here. If you have a rough
354
+ idea of the height your section, you can set `height` and `mode="cover"` which
355
+ will prevent, for example, a portrait orientation image from being retrieved and
356
+ cropped by the browser.
357
+
358
+ Since the z-index is set higher on the `div` containing the content, it will
359
+ show above the image. This example also sets `user-select: none` on the image to
360
+ prevent the image from being selected when the user clicks and drags on the page
361
+ to make it behave more like a traditional background image.
362
+
363
+ ### Fetching data from Sanity via GROQ
364
+
365
+ If you're using Sanity's GROQ query language to fetch data, here is how I
366
+ recommend fetching the fields you need from a typical image with the hotspot,
367
+ crop, and low-quality image preview included:
368
+
369
+ ```groq
370
+ "id": asset._ref,
371
+ "preview": asset->metadata.lqip,
372
+ hotspot { x, y },
373
+ crop {
374
+ bottom,
375
+ left,
376
+ right,
377
+ top,
378
+ }
379
+ ```
380
+
381
+ ## License
382
+
383
+ Copyright ©2023 Corey Ward. Available under the
384
+ [MIT License](https://github.com/coreyward/sanity-image/blob/main/LICENSE).
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { ImageWithPreviewProps } from "./types";
2
+ import type { ImageWithPreviewProps } from "./types";
3
3
  /**
4
4
  * Renders two image tags, one with the preview image and one with the full
5
5
  * image. When the full image is loaded, the preview image is removed, revealing
@@ -1,3 +1,3 @@
1
1
  import React, { type ReactElement } from "react";
2
- import { PolymorphicComponentProp, SanityImageProps } from "./types";
3
- export declare const SanityImage: <T extends React.ElementType<any> = "img">({ as: component, builder, asset, hotspot, crop, width, height, htmlWidth, htmlHeight, options, config, _type, _key, alt, ...rest }: PolymorphicComponentProp<T, SanityImageProps>) => ReactElement;
2
+ import type { PolymorphicComponentProps, SanityImageProps } from "./types";
3
+ export declare const SanityImage: <C extends React.ElementType<any> = "img">({ as: component, baseUrl, projectId, dataset, id, hotspot, crop, width, height, mode, preview, htmlWidth, htmlHeight, htmlId, queryParams, ...rest }: PolymorphicComponentProps<C, SanityImageProps>) => ReactElement;
package/dist/cjs/index.js CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";var q=Object.create;var b=Object.defineProperty;var H=Object.getOwnPropertyDescriptor;var D=Object.getOwnPropertyNames;var G=Object.getPrototypeOf,Y=Object.prototype.hasOwnProperty;var J=(e,r)=>{for(var t in r)b(e,t,{get:r[t],enumerable:!0})},C=(e,r,t,i)=>{if(r&&typeof r=="object"||typeof r=="function")for(let o of D(r))!Y.call(e,o)&&o!==t&&b(e,o,{get:()=>r[o],enumerable:!(i=H(r,o))||i.enumerable});return e};var U=(e,r,t)=>(t=e!=null?q(G(e)):{},C(r||!e||!e.__esModule?b(t,"default",{value:e,enumerable:!0}):t,e)),K=e=>C(b({},"__esModule",{value:!0}),e);var Z={};J(Z,{ImageWithPreview:()=>y,SanityImage:()=>L,buildSource:()=>B,buildSourceSet:()=>S,createBuilder:()=>W,imageUrl:()=>f,parseImageRef:()=>c});module.exports=K(Z);var M=U(require("@sanity/image-url")),W=({dataset:e,projectId:r})=>(0,M.default)({dataset:e,projectId:r});var Q=/^image-([\da-f]+)-(\d+x\d+)-(\w+)$/,c=e=>{let r=Q.exec(e),[,t,i,o]=r!=null?r:[];if(!r||!t||!i||!o)throw new Error(`Could not parse image ID "${e}"`);let[a,n]=i.split("x").map(g=>Number.parseInt(g,10));if(Number.isNaN(a)||Number.isNaN(n)||!a||!n)throw new Error(`Invalid dimensions "${i}"`);return{assetId:t,dimensions:{height:n,width:a},format:o}};var j={auto:"format",fit:"max",quality:75},B=(e,r,{width:t,height:i,...o})=>{let{dimensions:a}=c(e._id),n=a.width/a.height;return t=t||a.width,i=i||Math.round(t/n),f(e,r,{...o,height:i,width:t})},S=(e,r,t)=>{var R,x,w;let{dimensions:i}=c(e._id),o=(R=t.fit)!=null?R:j.fit,a=i.width/i.height,n=(x=t.width)!=null?x:i.width,g=(w=t.height)!=null?w:Math.round(n/a),p=n/g,P=a,h=i.width,E=i.height;if(e.crop&&Object.values(e.crop).some(m=>m>0)){let m=i.width-e.crop.left*i.width-e.crop.right*i.width,s=i.height-e.crop.top*i.height-e.crop.bottom*i.height;P=m/s,P>a?E=s:h=m}return Object.values([.5,.75,1,1.5,2].reduce((m,s)=>{let l=f(e,r,{...t,dpr:s}),u=Math.round(o&&["fillmax","max","min"].includes(o)?p<a?Math.min(E/(g*s)*(n*s),n*s):Math.min(n*s,h):n*s);return m[u]||(m[u]=`${l} ${u}w`),m},{})).join(", ")},f=(e,r,t={})=>{let i={...j,...t},o=r.image(e);return V(Object.keys(i),o),Object.entries(i).forEach(([a,n])=>{o=Array.isArray(n)?o[a](...n):o[a](n)}),o.url()};function V(e,r){if(!e.every(t=>X(t,r)))throw new Error("Invalid image URL parameter provided")}function X(e,r){return e in r}var d=U(require("react")),y=({as:e,preview:r,...t})=>{let[i,o]=(0,d.useState)(!1),a=(0,d.useRef)(null),n=()=>{o(!0)};(0,d.useEffect)(()=>{var p;(p=a.current)!=null&&p.complete&&n()});let g=e||"img";return d.default.createElement(d.default.Fragment,null,!i&&d.default.createElement(g,{alt:t.alt,className:t.className,"data-lqip":!0,height:t.height,id:t.id,src:r,style:t.style,width:t.width}),d.default.createElement(g,{"data-loading":i?null:!0,onLoad:n,ref:a,style:i?void 0:{height:"10px !important",opacity:0,pointerEvents:"none",position:"absolute",userSelect:"none",width:"10px !important",zIndex:-10},...t}))};var v=U(require("react"));var L=({as:e,builder:r,asset:t,hotspot:i,crop:o,width:a,height:n,htmlWidth:g,htmlHeight:p,options:P={},config:h={},_type:E,_key:R,alt:x="",...w})=>{var A,N,T;let m=e!=null?e:"img",s=(T=(A=t.metadata)==null?void 0:A.preview)!=null?T:(N=t.metadata)==null?void 0:N.lqip,l={...w,alt:x||""},u={_id:"_id"in t?t._id:t._ref,crop:o,hotspot:i};if(c(u._id).format==="svg")return v.default.createElement(m,{src:f(u,r),...l});let O=B(u,r,{...h,height:n,width:a}),$=S(u,r,{...h,height:n,width:a});if(P.aspectRatio){let{dimensions:I}=c(u._id);if(a&&n)l.width=a,l.height=n;else{o=o!=null?o:{bottom:0,left:0,right:0,top:0};let z=I.width*(1-o.left-o.right),F=I.height*(1-o.top-o.bottom),k=z/F;l.width=a!=null?a:I.width,l.height=Math.round(l.width/k)}}g&&(l.width=g),p&&(l.height=p);let ee=s?y:m,_={...l,loading:"lazy",src:O,srcSet:$,style:{...l.style,...i&&{objectPosition:[i.x,i.y].map(I=>(I*100).toFixed(2)+"%").join(" ")}}};return s?v.default.createElement(y,{..._,as:m,preview:s}):v.default.createElement(m,{..._})};0&&(module.exports={ImageWithPreview,SanityImage,buildSource,buildSourceSet,createBuilder,imageUrl,parseImageRef});
1
+ "use strict";var _=Object.create;var x=Object.defineProperty;var B=Object.getOwnPropertyDescriptor;var G=Object.getOwnPropertyNames;var J=Object.getPrototypeOf,Y=Object.prototype.hasOwnProperty;var k=(r,t)=>{for(var e in t)x(r,e,{get:t[e],enumerable:!0})},T=(r,t,e,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of G(t))!Y.call(r,o)&&o!==e&&x(r,o,{get:()=>t[o],enumerable:!(n=B(t,o))||n.enumerable});return r};var Q=(r,t,e)=>(e=r!=null?_(J(r)):{},T(t||!r||!r.__esModule?x(e,"default",{value:r,enumerable:!0}):e,r)),F=r=>T(x({},"__esModule",{value:!0}),r);var V={};k(V,{ImageWithPreview:()=>S,SanityImage:()=>O,buildSrc:()=>h,buildSrcSet:()=>P,parseImageId:()=>y});module.exports=F(V);var H=/^image-([\da-f]+)-(\d+x\d+)-(\w+)$/,y=r=>{let t=H.exec(r),[,e,n,o]=t!=null?t:[];if(!t||!e||!n||!o)throw new Error(`Could not parse image ID "${r}"`);let[a,m]=n.split("x").map(d=>Number.parseInt(d,10));if(Number.isNaN(a)||Number.isNaN(m)||!a||!m)throw new Error(`Invalid dimensions "${n}"`);return{assetId:e,dimensions:{height:m,width:a,aspectRatio:a/m},format:o}},w=r=>{let t=r.lastIndexOf("-");return r.slice(6,t)+"."+r.slice(t+1)};var h=({baseUrl:r,...t})=>{let{metadata:e,...n}=M({...t,options:{includeMetadata:!0}});if(!e)throw new Error("Missing image output metadata");return{src:`${`${r}${w(t.id)}`}?${L(n)}`,width:e.outputDimensions.width,height:e.outputDimensions.height}},P=({id:r,mode:t="contain",width:e,height:n,hotspot:o,crop:a,baseUrl:m})=>{let{w:d,h:u}=M({id:r,mode:t,width:e,height:n,hotspot:o,crop:a}),I=`${m}${w(r)}`,g=K(d).map(s=>{let i=Math.round(d*s),c=u&&Math.round(u*s);if(s<1&&i<50)return null;let l=M({id:r,mode:t,width:i,height:c,hotspot:o,crop:a});return`${I}?${L(l)} ${l.w}w`}).filter(Boolean);return Array.from(new Set(g))},q=({id:r,baseUrl:t})=>{let{assetId:e,dimensions:n,format:o}=y(r);return{src:`${t}${e}-${n.width}x${n.height}.${o}`,width:n.width,height:n.height}},K=r=>r<160?[.5,1,2]:r<750?[.5,1,1.5,2]:r<1400?[.25,.5,.75,1,1.5,2]:[.25,.5,.75,1,1.25,1.5,1.75,2],M=({id:r,mode:t="contain",width:e,height:n,hotspot:o,crop:a,queryParams:m,options:{includeMetadata:d=!1}={}})=>{let u=y(r).dimensions,{width:I,height:g,aspectRatio:s}=a?j(u,a):u;if(e||(n?(e=Math.round(n*s),n=void 0):e=Math.round(I/2)),t==="cover"&&(!e||!n||e/n===s)?t="contain":t==="contain"&&n&&(e=Math.min(e,Math.round(n*s)),n=void 0),e>I||n&&n>g){let c=n?e/n:s;c>=s?(e=I,n=n&&Math.round(e/c)):(n=g,e=Math.round(n*c))}let i={w:e,q:75,...m};if(i.fm||(i.auto="format"),a&&(i.rect=U(u,a)),t==="cover")if(i.fit="crop",n&&(i.h=n),o){let c=a?o.x/(1-a.left-a.right):o.x,l=a?o.y/(1-a.top-a.bottom):o.y;i["fp-x"]=W(A(c,0,1),3),i["fp-y"]=W(A(l,0,1),3)}else i.crop="entropy";else i.fit="max";if(d){let c=n||Math.round(e/s);i.metadata={sourceDimensions:u,outputDimensions:{width:e,height:c,aspectRatio:e/c}}}return i},A=(r,t,e)=>Math.max(t,Math.min(e,r)),W=(r,t)=>Math.round(r*Math.pow(10,t))/Math.pow(10,t),j=(r,t)=>{if(t.left+t.right>=1||t.top+t.bottom>=1)throw new Error(`Invalid crop: ${JSON.stringify(t)}; crop values must be less than 1`);let e=Math.round(r.width*(1-t.left-t.right)),n=Math.round(r.height*(1-t.top-t.bottom)),o=e/n;return{width:e,height:n,aspectRatio:o}},U=(r,t)=>{let{width:e,height:n}=j(r,t);return[Math.round(t.left*r.width),Math.round(t.top*r.height),e,n].join(",")},L=r=>Object.entries(r).sort(([t],[e])=>t.localeCompare(e)).map(([t,e])=>`${encodeURIComponent(t)}=${encodeURIComponent(e)}`).join("&").replace(/%2C/g,",");var p=Q(require("react")),S=({as:r,preview:t,...e})=>{let[n,o]=(0,p.useState)(!1),a=(0,p.useRef)(null),m=()=>{o(!0)};(0,p.useEffect)(()=>{var u;(u=a.current)!=null&&u.complete&&m()});let d=r||"img";return p.default.createElement(p.default.Fragment,null,!n&&p.default.createElement(d,{alt:e.alt,className:e.className,"data-lqip":!0,height:e.height,id:e.id,src:t,style:e.style,width:e.width}),p.default.createElement(d,{"data-loading":n?null:!0,onLoad:m,ref:a,style:n?void 0:{height:"10px !important",opacity:0,pointerEvents:"none",position:"absolute",userSelect:"none",width:"10px !important",zIndex:-10},...e}))};var $=Q(require("react"));var O=({as:r,baseUrl:t,projectId:e,dataset:n,id:o,hotspot:a,crop:m,width:d,height:u,mode:I="contain",preview:g,htmlWidth:s,htmlHeight:i,htmlId:c,queryParams:l,...b})=>{var D,N;if(!o)throw new Error("Missing required `id` prop for <SanityImage>.");if(!t&&(!e||!n))throw new Error("Missing required `baseUrl` or `projectId` and `dataset` props for <SanityImage>.");t=t!=null?t:`https://cdn.sanity.io/images/${e}/${n}/`;let C=o.endsWith("-svg"),E=g&&!C?S:r!=null?r:"img",f={alt:(D=b.alt)!=null?D:"",loading:(N=b.loading)!=null?N:"lazy",id:c,...b};if(C)return $.default.createElement(E,{...q({id:o,baseUrl:t}),...f});let v={baseUrl:t,id:o,crop:m,hotspot:a,width:d,height:u,mode:I,queryParams:l},{src:z,...R}=h(v);return f.srcSet=P(v).join(", "),f.src=z,f.width=s!=null?s:R.width,f.height=i!=null?i:R.height,g&&(f.as=r!=null?r:"img",f.preview=g),$.default.createElement(E,{...f})};0&&(module.exports={ImageWithPreview,SanityImage,buildSrc,buildSrcSet,parseImageId});
2
2
  //# sourceMappingURL=index.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../../src/index.ts", "../../src/builder.ts", "../../src/parseImageRef.ts", "../../src/imageUrls.ts", "../../src/ImageWithPreview.tsx", "../../src/SanityImage.tsx"],
4
- "sourcesContent": ["export { createBuilder } from \"./builder\"\nexport { imageUrl, buildSource, buildSourceSet } from \"./imageUrls\"\nexport { ImageWithPreview } from \"./ImageWithPreview\"\nexport { parseImageRef } from \"./parseImageRef\"\nexport { SanityImage } from \"./SanityImage\"\n", "import sanityImageUrl from \"@sanity/image-url\"\n\ntype BuilderConfig = {\n dataset: string\n projectId: string\n}\n\nexport const createBuilder = ({ dataset, projectId }: BuilderConfig) =>\n sanityImageUrl({ dataset, projectId })\n", "import { ImageRefParts } from \"./types\"\n\nexport const SANITY_REF_PATTERN = /^image-([\\da-f]+)-(\\d+x\\d+)-(\\w+)$/\n\n/**\n * Parse an image reference string into its component parts.\n *\n * @param {string} id The image reference string to parse in the format `image-<hash>-<width>x<height>.<ext>`\n * @returns {ImageRefParts} An object containing the asset ID, dimensions, and format\n */\nexport const parseImageRef = (id: string): ImageRefParts => {\n const match = SANITY_REF_PATTERN.exec(id)\n const [, assetId, dimensions, format] = match ?? []\n\n if (!match || !assetId || !dimensions || !format) {\n throw new Error(`Could not parse image ID \"${id}\"`)\n }\n\n const [width, height] = dimensions\n .split(\"x\")\n .map((value: string): number => Number.parseInt(value, 10))\n\n if (Number.isNaN(width) || Number.isNaN(height) || !width || !height) {\n throw new Error(`Invalid dimensions \"${dimensions}\"`)\n }\n\n return {\n assetId,\n dimensions: { height, width },\n format,\n }\n}\n", "import { type ImageUrlBuilder } from \"@sanity/image-url/lib/types/builder\"\nimport { parseImageRef } from \"./parseImageRef\"\nimport { Asset, ImageBuilderParameter, ImageBuilderParameters } from \"./types\"\n\nexport const DEFAULT_IMAGE_CONFIG: ImageBuilderParameters = {\n auto: \"format\",\n fit: \"max\",\n quality: 75,\n}\n\n/**\n * Returns the default `src` for the image based on the expected width (and\n * height) of the displayed image.\n */\nexport const buildSource = (\n asset: Asset,\n builder: ImageUrlBuilder,\n { width, height, ...config }: ImageBuilderParameters\n): string => {\n const { dimensions } = parseImageRef(asset._id)\n\n const origRatio = dimensions.width / dimensions.height\n width = width || dimensions.width\n height = height || Math.round(width / origRatio)\n\n return imageUrl(asset, builder, {\n ...config,\n height,\n width,\n })\n}\n\n/**\n * Returns a `srcSet` string for the image based on the expected width. The\n * generated `srcSet` will include images at 0.5x, 0.75x, 1x, 1.5x, and 2x the\n * expected width.\n *\n * This method also considers the FitMode configuration. If Sanity will not\n * scale up the image in the supplied mode, the `srcSet` will not include links\n * to images larger than the original image.\n */\nexport const buildSourceSet = (\n asset: Asset,\n builder: ImageUrlBuilder,\n config: ImageBuilderParameters\n): string => {\n const { dimensions } = parseImageRef(asset._id)\n const fitMode = config.fit ?? DEFAULT_IMAGE_CONFIG.fit\n\n // Determine dimensions and ratios for srcSet calculations\n const origRatio = dimensions.width / dimensions.height\n const width = config.width ?? dimensions.width\n const height = config.height ?? Math.round(width / origRatio)\n const targetRatio = width / height\n let cropRatio = origRatio\n let maxWidth = dimensions.width\n let maxHeight = dimensions.height\n\n // Compensate for dimensional changes if image was cropped in Sanity\n if (asset.crop && Object.values(asset.crop).some((n) => n > 0)) {\n const cropWidth =\n dimensions.width -\n asset.crop.left * dimensions.width -\n asset.crop.right * dimensions.width\n const cropHeight =\n dimensions.height -\n asset.crop.top * dimensions.height -\n asset.crop.bottom * dimensions.height\n\n cropRatio = cropWidth / cropHeight\n if (cropRatio > origRatio) {\n maxHeight = cropHeight\n } else {\n maxWidth = cropWidth\n }\n }\n\n return Object.values(\n [0.5, 0.75, 1, 1.5, 2].reduce((set: { [size: number]: string }, dpr) => {\n const url = imageUrl(asset, builder, { ...config, dpr })\n const size = Math.round(\n // For modes where Sanity will not scale up, determine\n // the anticipated final width based on the params\n fitMode && [\"fillmax\", \"max\", \"min\"].includes(fitMode)\n ? targetRatio < origRatio\n ? Math.min(\n (maxHeight / (height * dpr)) * (width * dpr),\n width * dpr\n )\n : Math.min(width * dpr, maxWidth)\n : width * dpr\n )\n\n // Avoid duplicate sizes in srcSet list\n if (!set[size]) {\n set[size] = `${url} ${size}w`\n }\n\n return set\n }, {})\n ).join(\", \")\n}\n\nexport const imageUrl = (\n asset: Asset,\n builder: ImageUrlBuilder,\n parameters: ImageBuilderParameters = {}\n): string => {\n const combinedParameters = { ...DEFAULT_IMAGE_CONFIG, ...parameters }\n let imageUrlBuilder = builder.image(asset)\n\n validateImageUrlParameters(Object.keys(combinedParameters), imageUrlBuilder)\n\n Object.entries(combinedParameters).forEach(([key, value]) => {\n imageUrlBuilder = Array.isArray(value)\n ? (imageUrlBuilder as any)[key](...value)\n : (imageUrlBuilder as any)[key](value)\n })\n\n return imageUrlBuilder.url()\n}\n\nfunction validateImageUrlParameters(\n keys: string[],\n imageUrlBuilder: ImageUrlBuilder\n): asserts keys is ImageBuilderParameter[] {\n if (!keys.every((key) => validateImageUrlParameter(key, imageUrlBuilder))) {\n throw new Error(`Invalid image URL parameter provided`)\n }\n}\n\nfunction validateImageUrlParameter(\n key: string,\n imageUrlBuilder: ImageUrlBuilder\n): key is ImageBuilderParameter {\n return key in imageUrlBuilder\n}\n", "import React, { useEffect, useRef, useState } from \"react\"\nimport { ImageWithPreviewProps } from \"./types\"\n\n/**\n * Renders two image tags, one with the preview image and one with the full\n * image. When the full image is loaded, the preview image is removed, revealing\n * the full image.\n */\nexport const ImageWithPreview = <T extends React.ElementType = \"img\">({\n as,\n preview,\n ...props\n}: ImageWithPreviewProps<T>) => {\n const [loaded, setLoaded] = useState(false)\n const ref = useRef<HTMLImageElement>(null)\n\n const onLoad = () => {\n setLoaded(true)\n }\n\n useEffect(() => {\n if (ref.current?.complete) {\n onLoad()\n }\n })\n\n const Img = as || \"img\"\n\n return (\n <>\n {!loaded && (\n <Img\n alt={props.alt}\n className={props.className}\n data-lqip\n height={props.height}\n id={props.id}\n src={preview}\n style={props.style}\n width={props.width}\n />\n )}\n <Img\n data-loading={loaded ? null : true}\n onLoad={onLoad}\n ref={ref}\n style={\n loaded\n ? undefined\n : {\n // must be > 4px to be lazy loaded\n height: \"10px !important\",\n\n // must be > 4px to be lazy loaded\n opacity: 0,\n\n pointerEvents: \"none\",\n // Cannot use negative x or y values, visibility: hidden, or display: none\n // to hide or the image might not get loaded.\n position: \"absolute\",\n userSelect: \"none\",\n width: \"10px !important\",\n zIndex: -10,\n }\n }\n {...props}\n />\n </>\n )\n}\n", "import React, {\n type ReactElement,\n type ElementType,\n type ComponentPropsWithoutRef,\n} from \"react\"\nimport { Asset, PolymorphicComponentProp, SanityImageProps } from \"./types\"\nimport { parseImageRef } from \"./parseImageRef\"\nimport { buildSource, buildSourceSet, imageUrl } from \"./imageUrls\"\nimport { ImageWithPreview } from \"./ImageWithPreview\"\n\nexport const SanityImage = <T extends ElementType = \"img\">({\n as: component,\n builder,\n\n asset,\n hotspot,\n crop,\n\n width,\n height,\n htmlWidth,\n htmlHeight,\n\n options = {},\n config = {},\n\n // Swallowing these params for convenience\n _type,\n _key,\n\n alt = \"\",\n ...rest\n}: PolymorphicComponentProp<T, SanityImageProps>): ReactElement => {\n const ImageComponent = component ?? \"img\"\n const preview = asset.metadata?.preview ?? asset.metadata?.lqip\n\n const props: typeof rest & {\n alt: string\n width?: number\n height?: number\n } = {\n ...rest,\n alt: alt || \"\",\n }\n\n // Rebuild `asset` with only the properties needed for the image\n const image: Asset = {\n _id: \"_id\" in asset ? asset._id : asset._ref,\n crop,\n hotspot,\n }\n\n // Short circuit for SVG images\n if (parseImageRef(image._id).format === \"svg\") {\n return <ImageComponent src={imageUrl(image, builder)} {...props} />\n }\n\n // Create default src and build srcSet\n const source = buildSource(image, builder, { ...config, height, width })\n const sourceSet = buildSourceSet(image, builder, { ...config, height, width })\n\n if (options.aspectRatio) {\n // If enabled, this will estimate the final aspect ratio based on\n // the dimensions of the original image and the crop parameter,\n // then use this aspect ratio to apply `width` and `height` attrs\n // to both the preview and final images.\n //\n // Note: No attempts are made to compensate for the `fit` mode or\n // image params that transform the final output dimensions in this\n // early proof-of-concept version.\n const { dimensions } = parseImageRef(image._id)\n\n // Short circuit if both width and height are set. This will result\n // in the final aspect ratio matching the aspect ration of the\n // provided width and height props, ignoring the image dimensions.\n //\n // This relies on a bug in the @sanity/image-url library that\n // results in images being cropped with fit modes where they\n // should not be.\n if (width && height) {\n props.width = width\n props.height = height\n } else {\n // If `crop` isn't set, use fallback values.\n crop = crop ?? { bottom: 0, left: 0, right: 0, top: 0 }\n\n const croppedWidth = dimensions.width * (1 - crop.left - crop.right)\n const croppedHeight = dimensions.height * (1 - crop.top - crop.bottom)\n const ratio = croppedWidth / croppedHeight\n\n props.width = width ?? dimensions.width\n props.height = Math.round(props.width / ratio)\n }\n }\n\n if (htmlWidth) props.width = htmlWidth\n if (htmlHeight) props.height = htmlHeight\n\n const Image = preview ? ImageWithPreview : ImageComponent\n\n const componentProps: ComponentPropsWithoutRef<typeof Image> = {\n ...props,\n loading: \"lazy\",\n src: source,\n srcSet: sourceSet,\n style: {\n ...props.style,\n ...(hotspot && {\n objectPosition: [hotspot.x, hotspot.y]\n .map((value) => (value * 100).toFixed(2) + \"%\")\n .join(\" \"),\n }),\n },\n }\n\n return preview ? (\n <ImageWithPreview\n {...componentProps}\n as={ImageComponent}\n preview={preview}\n />\n ) : (\n <ImageComponent {...componentProps} />\n )\n}\n"],
5
- "mappings": "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",
6
- "names": ["src_exports", "__export", "ImageWithPreview", "SanityImage", "buildSource", "buildSourceSet", "createBuilder", "imageUrl", "parseImageRef", "__toCommonJS", "import_image_url", "createBuilder", "dataset", "projectId", "sanityImageUrl", "SANITY_REF_PATTERN", "parseImageRef", "id", "match", "assetId", "dimensions", "format", "width", "height", "value", "DEFAULT_IMAGE_CONFIG", "buildSource", "asset", "builder", "width", "height", "config", "dimensions", "parseImageRef", "origRatio", "imageUrl", "buildSourceSet", "_a", "_b", "_c", "fitMode", "targetRatio", "cropRatio", "maxWidth", "maxHeight", "n", "cropWidth", "cropHeight", "set", "dpr", "url", "size", "parameters", "combinedParameters", "imageUrlBuilder", "validateImageUrlParameters", "key", "value", "keys", "validateImageUrlParameter", "import_react", "ImageWithPreview", "as", "preview", "props", "loaded", "setLoaded", "ref", "onLoad", "_a", "Img", "React", "import_react", "SanityImage", "component", "builder", "asset", "hotspot", "crop", "width", "height", "htmlWidth", "htmlHeight", "options", "config", "_type", "_key", "alt", "rest", "_a", "_b", "_c", "ImageComponent", "preview", "props", "image", "parseImageRef", "React", "imageUrl", "source", "buildSource", "sourceSet", "buildSourceSet", "dimensions", "croppedWidth", "croppedHeight", "ratio", "Image", "ImageWithPreview", "componentProps", "value"]
3
+ "sources": ["../../src/index.ts", "../../src/parseImageId.ts", "../../src/urlBuilder.ts", "../../src/ImageWithPreview.tsx", "../../src/SanityImage.tsx"],
4
+ "sourcesContent": ["export { buildSrc, buildSrcSet } from \"./urlBuilder\"\nexport { ImageWithPreview } from \"./ImageWithPreview\"\nexport { parseImageId } from \"./parseImageId\"\nexport { SanityImage } from \"./SanityImage\"\n", "import type { ImageIdParts } from \"./types\"\n\nexport const SANITY_IMAGE_ID_PATTERN = /^image-([\\da-f]+)-(\\d+x\\d+)-(\\w+)$/\n\n/**\n * Parse an image id string into its component parts.\n *\n * @param {string} id The image id string to parse in the format `image-<hash>-<width>x<height>.<ext>`\n * @returns {ImageIdParts} An object containing the asset ID, dimensions, and format\n */\nexport const parseImageId = (id: string): ImageIdParts => {\n const match = SANITY_IMAGE_ID_PATTERN.exec(id)\n const [, assetId, dimensions, format] = match ?? []\n\n if (!match || !assetId || !dimensions || !format) {\n throw new Error(`Could not parse image ID \"${id}\"`)\n }\n\n const [width, height] = dimensions\n .split(\"x\")\n .map((value: string): number => Number.parseInt(value, 10))\n\n if (Number.isNaN(width) || Number.isNaN(height) || !width || !height) {\n throw new Error(`Invalid dimensions \"${dimensions}\"`)\n }\n\n return {\n assetId,\n dimensions: { height, width, aspectRatio: width / height },\n format,\n }\n}\n\n/**\n * Convert an image id to a URL path segment for the Sanity Image API. Input is\n * not validated.\n *\n * @example\n * imageIdToUrlPath(\"image-<hash>-<width>x<height>-<ext>\")\n * // => \"<hash>-<width>x<height>.<ext>\"\n */\nexport const imageIdToUrlPath = (id: string): string => {\n // This can be implemented with `parseImageId` but it's more computationally expensive\n // than this more naive implementation.\n\n const formatSeparatorIndex = id.lastIndexOf(\"-\")\n\n return (\n id.slice(6, formatSeparatorIndex) + \".\" + id.slice(formatSeparatorIndex + 1)\n )\n}\n", "import { imageIdToUrlPath, parseImageId } from \"./parseImageId\"\nimport type {\n ComputedImageData,\n CropData,\n ImageIdParts,\n ImageQueryInputs,\n ImageQueryParams,\n ImageSrcInputs,\n} from \"types\"\n\n/**\n * Convert ImageSrcInputs into a full image URL and computed output dimensions.\n */\nexport const buildSrc = ({\n baseUrl,\n ...inputParams\n}: ImageSrcInputs): ComputedImageData => {\n const { metadata, ...queryParams } = buildQueryParams({\n ...inputParams,\n options: { includeMetadata: true },\n })\n\n // Narrowing for TS\n if (!metadata) {\n throw new Error(\"Missing image output metadata\")\n }\n\n const imageUrl = `${baseUrl}${imageIdToUrlPath(inputParams.id)}`\n\n return {\n src: `${imageUrl}?${buildQueryString(queryParams)}`,\n width: metadata.outputDimensions.width,\n height: metadata.outputDimensions.height,\n }\n}\n\nexport const buildSrcSet = ({\n id,\n mode = \"contain\",\n width,\n height,\n hotspot,\n crop,\n baseUrl,\n}: ImageSrcInputs) => {\n // Determine base computed width\n const { w, h } = buildQueryParams({ id, mode, width, height, hotspot, crop })\n\n // URL of the image without any query parameters\n const imageUrl = `${baseUrl}${imageIdToUrlPath(id)}`\n\n // Build srcset\n const srcSetEntries = dynamicMultipliers(w)\n .map((multiple) => {\n const computedWidth = Math.round(w * multiple)\n const computedHeight = h && Math.round(h * multiple)\n\n // Ignore tiny entries; the extra data in the HTML is almost never worth it\n if (multiple < 1 && computedWidth < 50) return null\n\n const params: Omit<ImageQueryParams, \"metadata\"> = buildQueryParams({\n id,\n mode,\n width: computedWidth,\n height: computedHeight,\n hotspot,\n crop,\n })\n\n return `${imageUrl}?${buildQueryString(params)} ${params.w}w`\n })\n .filter(Boolean)\n\n return Array.from(new Set(srcSetEntries))\n}\n\nexport const buildSvgAttributes = ({ id, baseUrl }: ImageSrcInputs) => {\n const { assetId, dimensions, format } = parseImageId(id)\n\n return {\n src: `${baseUrl}${assetId}-${dimensions.width}x${dimensions.height}.${format}`,\n width: dimensions.width,\n height: dimensions.height,\n }\n}\n\nconst dynamicMultipliers = (width: number) => {\n // For really small images, use larger steps\n if (width < 160) {\n return [0.5, 1, 2]\n }\n\n // For typical width images, use standard steps\n if (width < 750) {\n return [0.5, 1, 1.5, 2]\n }\n\n // For larger images, include 0.25x and 0.75x steps\n if (width < 1400) {\n return [0.25, 0.5, 0.75, 1, 1.5, 2]\n }\n\n // For really large images, use a wider range of steps at the low end, and smaller steps at the high end\n return [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]\n}\n\n/**\n * Constructs a query parameters object for the Sanity image URL based on the inputs provided.\n */\nexport const buildQueryParams = ({\n id,\n mode = \"contain\",\n width,\n height,\n hotspot,\n crop,\n queryParams,\n options: { includeMetadata = false } = {},\n}: ImageQueryInputs & {\n options?: {\n /** Include data about the image in the response */\n includeMetadata?: boolean\n }\n}): ImageQueryParams => {\n const sourceDimensions = parseImageId(id).dimensions\n\n // If crop is provided, compute post-crop dimensions\n const {\n width: maxWidth,\n height: maxHeight,\n aspectRatio: sourceAspectRatio,\n } = crop ? croppedImageSize(sourceDimensions, crop) : sourceDimensions\n\n // Determine width if not provided\n if (!width) {\n if (height) {\n // Compute width based on height and default ratio\n width = Math.round(height * sourceAspectRatio)\n\n // Discard `height` since we have to be in `contain` mode and we've converted it into `width`\n height = undefined\n } else {\n // Use 1/2 of the max image width by default to allow for 2x scale-up\n width = Math.round(maxWidth / 2)\n }\n }\n\n // Override `cover` mode if both width and height haven't been provided, or if\n // the requested aspect ratio matches the source aspect ratio. In these cases\n // the result will be the same as `contain` mode anyways, and `contain` mode\n // is simpler and saves a few bytes in the URL.\n if (\n mode === \"cover\" &&\n (!width || !height || width / height === sourceAspectRatio)\n ) {\n mode = \"contain\"\n } else if (mode === \"contain\" && height) {\n // Similarly, if `contain` mode is used and a height is provided, we can\n // convert it into a width by adjusting the width such that the\n // aspect-ratio\u2013constrained result will respect the height provided.\n width = Math.min(width, Math.round(height * sourceAspectRatio))\n height = undefined\n }\n\n // Clamp min and max dimensions while preserving requested aspect ratio\n if (width > maxWidth || (height && height > maxHeight)) {\n const requestedAspectRatio = height ? width / height : sourceAspectRatio\n\n if (requestedAspectRatio >= sourceAspectRatio) {\n // Clamp width\n width = maxWidth\n height = height && Math.round(width / requestedAspectRatio)\n } else {\n // Clamp height\n height = maxHeight\n width = Math.round(height * requestedAspectRatio)\n }\n }\n\n // Note: when converting params to a query string initially, we need to\n // use an object or map instead of URLSearchParams, since the latter will\n // allow multiple params with the same name, which is not supported by the\n // Sanity Image API.\n const params: Partial<ImageQueryParams> = {\n w: width,\n q: 75,\n ...queryParams,\n }\n\n // If an explicit format has not been requested, use auto format\n if (!params.fm) params.auto = \"format\"\n\n if (crop) {\n // Convert crop to rect param)\n params.rect = buildRect(sourceDimensions, crop)\n }\n\n if (mode === \"cover\") {\n params.fit = \"crop\"\n\n if (height) {\n params.h = height\n }\n\n if (hotspot) {\n // Hotspot is relative to post-`rect` dimensions; if `crop` is present,\n // the hotspot inputs need to be adjusted accordingly\n const x = crop ? hotspot.x / (1 - crop.left - crop.right) : hotspot.x\n const y = crop ? hotspot.y / (1 - crop.top - crop.bottom) : hotspot.y\n\n params[\"fp-x\"] = roundWithPrecision(clamp(x, 0, 1), 3)\n params[\"fp-y\"] = roundWithPrecision(clamp(y, 0, 1), 3)\n } else {\n // If no hotspot is provided, use Sanity\u2019s `entropy` crop mode\n params.crop = \"entropy\"\n }\n } else {\n params.fit = \"max\"\n }\n\n if (includeMetadata) {\n // Height will be set if the aspect ratio varies from `sourceAspectRatio`\n const outputHeight = height || Math.round(width / sourceAspectRatio)\n\n params.metadata = {\n sourceDimensions,\n outputDimensions: {\n width,\n height: outputHeight,\n aspectRatio: width / outputHeight,\n },\n }\n }\n\n return <ImageQueryParams>params\n}\n\nconst clamp = (value: number, min: number, max: number) =>\n Math.max(min, Math.min(max, value))\nconst roundWithPrecision = (value: number, precision: number) =>\n Math.round(value * Math.pow(10, precision)) / Math.pow(10, precision)\n\nexport const croppedImageSize = (\n /** Source/original image dimensions */\n dimensions: { width: number; height: number },\n crop: CropData\n): ImageIdParts[\"dimensions\"] => {\n if (crop.left + crop.right >= 1 || crop.top + crop.bottom >= 1) {\n throw new Error(\n `Invalid crop: ${JSON.stringify(crop)}; crop values must be less than 1`\n )\n }\n\n const width = Math.round(dimensions.width * (1 - crop.left - crop.right))\n const height = Math.round(dimensions.height * (1 - crop.top - crop.bottom))\n const aspectRatio = width / height\n\n return { width, height, aspectRatio }\n}\n\n/**\n * Build a `rect` value to crop the image.\n */\nexport const buildRect = (\n /** Source/original image dimensions */\n dimensions: { width: number; height: number },\n crop: CropData\n) => {\n const { width, height } = croppedImageSize(dimensions, crop)\n\n return [\n Math.round(crop.left * dimensions.width),\n Math.round(crop.top * dimensions.height),\n width,\n height,\n ].join(\",\")\n}\n\nexport const buildQueryString = (params: Record<string, string | number>) =>\n Object.entries(params)\n .sort(([a], [b]) => a.localeCompare(b))\n .map(\n ([key, value]) =>\n `${encodeURIComponent(key)}=${encodeURIComponent(value)}`\n )\n .join(\"&\")\n .replace(/%2C/g, \",\") // don't urlencode commas\n", "import React, { useEffect, useRef, useState } from \"react\"\nimport type { ImageWithPreviewProps } from \"./types\"\n\n/**\n * Renders two image tags, one with the preview image and one with the full\n * image. When the full image is loaded, the preview image is removed, revealing\n * the full image.\n */\nexport const ImageWithPreview = <T extends React.ElementType = \"img\">({\n as,\n preview,\n ...props\n}: ImageWithPreviewProps<T>) => {\n const [loaded, setLoaded] = useState(false)\n const ref = useRef<HTMLImageElement>(null)\n\n const onLoad = () => {\n setLoaded(true)\n }\n\n useEffect(() => {\n if (ref.current?.complete) {\n onLoad()\n }\n })\n\n const Img = as || \"img\"\n\n return (\n <>\n {!loaded && (\n <Img\n alt={props.alt}\n className={props.className}\n data-lqip\n height={props.height}\n id={props.id}\n src={preview}\n style={props.style}\n width={props.width}\n />\n )}\n <Img\n data-loading={loaded ? null : true}\n onLoad={onLoad}\n ref={ref}\n style={\n loaded\n ? undefined\n : {\n // must be > 4px to be lazy loaded\n height: \"10px !important\",\n\n // must be > 4px to be lazy loaded\n opacity: 0,\n\n pointerEvents: \"none\",\n // Cannot use negative x or y values, visibility: hidden, or display: none\n // to hide or the image might not get loaded.\n position: \"absolute\",\n userSelect: \"none\",\n width: \"10px !important\",\n zIndex: -10,\n }\n }\n {...props}\n />\n </>\n )\n}\n", "import React, {\n type ReactElement,\n type ElementType,\n type ComponentPropsWithoutRef,\n} from \"react\"\nimport type { PolymorphicComponentProps, SanityImageProps } from \"./types\"\nimport { buildSrc, buildSrcSet, buildSvgAttributes } from \"./urlBuilder\"\nimport { ImageWithPreview } from \"./ImageWithPreview\"\n\nexport const SanityImage = <C extends ElementType = \"img\">({\n as: component,\n\n // Sanity url\n baseUrl,\n projectId,\n dataset,\n\n // Image definition data\n id,\n hotspot,\n crop,\n width,\n height,\n mode = \"contain\",\n\n // Data for LQIP (preview image)\n preview,\n\n // Native-behavior overrides\n htmlWidth,\n htmlHeight,\n htmlId,\n\n // Image query string params\n queryParams,\n\n // Any remaining props are passed through to the rendered component\n ...rest\n}: PolymorphicComponentProps<C, SanityImageProps>): ReactElement => {\n if (!id) throw new Error(\"Missing required `id` prop for <SanityImage>.\")\n if (!baseUrl && (!projectId || !dataset))\n throw new Error(\n \"Missing required `baseUrl` or `projectId` and `dataset` props for <SanityImage>.\"\n )\n\n baseUrl = baseUrl ?? `https://cdn.sanity.io/images/${projectId}/${dataset}/`\n\n const isSvg = id.endsWith(\"-svg\")\n\n const ImageComponent =\n preview && !isSvg ? ImageWithPreview : component ?? \"img\"\n\n const componentProps: ComponentPropsWithoutRef<typeof ImageComponent> = {\n alt: rest.alt ?? \"\",\n loading: rest.loading ?? \"lazy\",\n id: htmlId,\n ...rest,\n }\n\n if (isSvg) {\n // Sanity ignores all transformations for SVGs, so we can just render the\n // component without passing a query string and without doing anything for\n // the preview.\n return (\n <ImageComponent\n {...buildSvgAttributes({ id, baseUrl })}\n {...componentProps}\n />\n )\n }\n\n // Create default src and build srcSet\n const srcParams = {\n baseUrl,\n id,\n crop,\n hotspot,\n width,\n height,\n mode,\n queryParams,\n }\n\n const { src, ...outputDimensions } = buildSrc(srcParams)\n componentProps.srcSet = buildSrcSet(srcParams).join(\", \")\n componentProps.src = src\n componentProps.width = htmlWidth ?? outputDimensions.width\n componentProps.height = htmlHeight ?? outputDimensions.height\n\n if (preview) {\n componentProps.as = component ?? \"img\"\n componentProps.preview = preview\n }\n\n return <ImageComponent {...componentProps} />\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["src_exports", "__export", "ImageWithPreview", "SanityImage", "buildSrc", "buildSrcSet", "parseImageId", "__toCommonJS", "SANITY_IMAGE_ID_PATTERN", "parseImageId", "id", "match", "assetId", "dimensions", "format", "width", "height", "value", "imageIdToUrlPath", "formatSeparatorIndex", "buildSrc", "baseUrl", "inputParams", "metadata", "queryParams", "buildQueryParams", "imageIdToUrlPath", "buildQueryString", "buildSrcSet", "id", "mode", "width", "height", "hotspot", "crop", "w", "h", "imageUrl", "srcSetEntries", "dynamicMultipliers", "multiple", "computedWidth", "computedHeight", "params", "buildSvgAttributes", "assetId", "dimensions", "format", "parseImageId", "includeMetadata", "sourceDimensions", "maxWidth", "maxHeight", "sourceAspectRatio", "croppedImageSize", "requestedAspectRatio", "buildRect", "x", "y", "roundWithPrecision", "clamp", "outputHeight", "value", "min", "max", "precision", "aspectRatio", "a", "b", "key", "import_react", "ImageWithPreview", "as", "preview", "props", "loaded", "setLoaded", "ref", "onLoad", "_a", "Img", "React", "import_react", "SanityImage", "component", "baseUrl", "projectId", "dataset", "id", "hotspot", "crop", "width", "height", "mode", "preview", "htmlWidth", "htmlHeight", "htmlId", "queryParams", "rest", "_a", "_b", "isSvg", "ImageComponent", "ImageWithPreview", "componentProps", "React", "buildSvgAttributes", "srcParams", "src", "outputDimensions", "buildSrc", "buildSrcSet"]
7
7
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,4 @@
1
- export { createBuilder } from "./builder";
2
- export { imageUrl, buildSource, buildSourceSet } from "./imageUrls";
1
+ export { buildSrc, buildSrcSet } from "./urlBuilder";
3
2
  export { ImageWithPreview } from "./ImageWithPreview";
4
- export { parseImageRef } from "./parseImageRef";
3
+ export { parseImageId } from "./parseImageId";
5
4
  export { SanityImage } from "./SanityImage";
package/dist/mjs/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import M from"@sanity/image-url";var W=({dataset:r,projectId:n})=>M({dataset:r,projectId:n});var j=/^image-([\da-f]+)-(\d+x\d+)-(\w+)$/,c=r=>{let n=j.exec(r),[,t,e,a]=n??[];if(!n||!t||!e||!a)throw new Error(`Could not parse image ID "${r}"`);let[i,o]=e.split("x").map(s=>Number.parseInt(s,10));if(Number.isNaN(i)||Number.isNaN(o)||!i||!o)throw new Error(`Invalid dimensions "${e}"`);return{assetId:t,dimensions:{height:o,width:i},format:a}};var U={auto:"format",fit:"max",quality:75},S=(r,n,{width:t,height:e,...a})=>{let{dimensions:i}=c(r._id),o=i.width/i.height;return t=t||i.width,e=e||Math.round(t/o),h(r,n,{...a,height:e,width:t})},v=(r,n,t)=>{let{dimensions:e}=c(r._id),a=t.fit??U.fit,i=e.width/e.height,o=t.width??e.width,s=t.height??Math.round(o/i),I=o/s,y=i,u=e.width,x=e.height;if(r.crop&&Object.values(r.crop).some(l=>l>0)){let l=e.width-r.crop.left*e.width-r.crop.right*e.width,m=e.height-r.crop.top*e.height-r.crop.bottom*e.height;y=l/m,y>i?x=m:u=l}return Object.values([.5,.75,1,1.5,2].reduce((l,m)=>{let b=h(r,n,{...t,dpr:m}),g=Math.round(a&&["fillmax","max","min"].includes(a)?I<i?Math.min(x/(s*m)*(o*m),o*m):Math.min(o*m,u):o*m);return l[g]||(l[g]=`${b} ${g}w`),l},{})).join(", ")},h=(r,n,t={})=>{let e={...U,...t},a=n.image(r);return L(Object.keys(e),a),Object.entries(e).forEach(([i,o])=>{a=Array.isArray(o)?a[i](...o):a[i](o)}),a.url()};function L(r,n){if(!r.every(t=>O(t,n)))throw new Error("Invalid image URL parameter provided")}function O(r,n){return r in n}import P,{useEffect as $,useRef as z,useState as F}from"react";var w=({as:r,preview:n,...t})=>{let[e,a]=F(!1),i=z(null),o=()=>{a(!0)};$(()=>{i.current?.complete&&o()});let s=r||"img";return P.createElement(P.Fragment,null,!e&&P.createElement(s,{alt:t.alt,className:t.className,"data-lqip":!0,height:t.height,id:t.id,src:n,style:t.style,width:t.width}),P.createElement(s,{"data-loading":e?null:!0,onLoad:o,ref:i,style:e?void 0:{height:"10px !important",opacity:0,pointerEvents:"none",position:"absolute",userSelect:"none",width:"10px !important",zIndex:-10},...t}))};import E from"react";var k=({as:r,builder:n,asset:t,hotspot:e,crop:a,width:i,height:o,htmlWidth:s,htmlHeight:I,options:y={},config:u={},_type:x,_key:l,alt:m="",...b})=>{let g=r??"img",B=t.metadata?.preview??t.metadata?.lqip,d={...b,alt:m||""},p={_id:"_id"in t?t._id:t._ref,crop:a,hotspot:e};if(c(p._id).format==="svg")return E.createElement(g,{src:h(p,n),...d});let _=S(p,n,{...u,height:o,width:i}),A=v(p,n,{...u,height:o,width:i});if(y.aspectRatio){let{dimensions:f}=c(p._id);if(i&&o)d.width=i,d.height=o;else{a=a??{bottom:0,left:0,right:0,top:0};let N=f.width*(1-a.left-a.right),T=f.height*(1-a.top-a.bottom),C=N/T;d.width=i??f.width,d.height=Math.round(d.width/C)}}s&&(d.width=s),I&&(d.height=I);let q=B?w:g,R={...d,loading:"lazy",src:_,srcSet:A,style:{...d.style,...e&&{objectPosition:[e.x,e.y].map(f=>(f*100).toFixed(2)+"%").join(" ")}}};return B?E.createElement(w,{...R,as:g,preview:B}):E.createElement(g,{...R})};export{w as ImageWithPreview,k as SanityImage,S as buildSource,v as buildSourceSet,W as createBuilder,h as imageUrl,c as parseImageRef};
1
+ var A=/^image-([\da-f]+)-(\d+x\d+)-(\w+)$/,I=n=>{let t=A.exec(n),[,e,r,o]=t??[];if(!t||!e||!r||!o)throw new Error(`Could not parse image ID "${n}"`);let[a,s]=r.split("x").map(c=>Number.parseInt(c,10));if(Number.isNaN(a)||Number.isNaN(s)||!a||!s)throw new Error(`Invalid dimensions "${r}"`);return{assetId:e,dimensions:{height:s,width:a,aspectRatio:a/s},format:o}},x=n=>{let t=n.lastIndexOf("-");return n.slice(6,t)+"."+n.slice(t+1)};var P=({baseUrl:n,...t})=>{let{metadata:e,...r}=b({...t,options:{includeMetadata:!0}});if(!e)throw new Error("Missing image output metadata");return{src:`${`${n}${x(t.id)}`}?${T(r)}`,width:e.outputDimensions.width,height:e.outputDimensions.height}},S=({id:n,mode:t="contain",width:e,height:r,hotspot:o,crop:a,baseUrl:s})=>{let{w:c,h:p}=b({id:n,mode:t,width:e,height:r,hotspot:o,crop:a}),l=`${s}${x(n)}`,d=q(c).map(m=>{let i=Math.round(c*m),u=p&&Math.round(p*m);if(m<1&&i<50)return null;let f=b({id:n,mode:t,width:i,height:u,hotspot:o,crop:a});return`${l}?${T(f)} ${f.w}w`}).filter(Boolean);return Array.from(new Set(d))},D=({id:n,baseUrl:t})=>{let{assetId:e,dimensions:r,format:o}=I(n);return{src:`${t}${e}-${r.width}x${r.height}.${o}`,width:r.width,height:r.height}},q=n=>n<160?[.5,1,2]:n<750?[.5,1,1.5,2]:n<1400?[.25,.5,.75,1,1.5,2]:[.25,.5,.75,1,1.25,1.5,1.75,2],b=({id:n,mode:t="contain",width:e,height:r,hotspot:o,crop:a,queryParams:s,options:{includeMetadata:c=!1}={}})=>{let p=I(n).dimensions,{width:l,height:d,aspectRatio:m}=a?N(p,a):p;if(e||(r?(e=Math.round(r*m),r=void 0):e=Math.round(l/2)),t==="cover"&&(!e||!r||e/r===m)?t="contain":t==="contain"&&r&&(e=Math.min(e,Math.round(r*m)),r=void 0),e>l||r&&r>d){let u=r?e/r:m;u>=m?(e=l,r=r&&Math.round(e/u)):(r=d,e=Math.round(r*u))}let i={w:e,q:75,...s};if(i.fm||(i.auto="format"),a&&(i.rect=j(p,a)),t==="cover")if(i.fit="crop",r&&(i.h=r),o){let u=a?o.x/(1-a.left-a.right):o.x,f=a?o.y/(1-a.top-a.bottom):o.y;i["fp-x"]=R(v(u,0,1),3),i["fp-y"]=R(v(f,0,1),3)}else i.crop="entropy";else i.fit="max";if(c){let u=r||Math.round(e/m);i.metadata={sourceDimensions:p,outputDimensions:{width:e,height:u,aspectRatio:e/u}}}return i},v=(n,t,e)=>Math.max(t,Math.min(e,n)),R=(n,t)=>Math.round(n*Math.pow(10,t))/Math.pow(10,t),N=(n,t)=>{if(t.left+t.right>=1||t.top+t.bottom>=1)throw new Error(`Invalid crop: ${JSON.stringify(t)}; crop values must be less than 1`);let e=Math.round(n.width*(1-t.left-t.right)),r=Math.round(n.height*(1-t.top-t.bottom)),o=e/r;return{width:e,height:r,aspectRatio:o}},j=(n,t)=>{let{width:e,height:r}=N(n,t);return[Math.round(t.left*n.width),Math.round(t.top*n.height),e,r].join(",")},T=n=>Object.entries(n).sort(([t],[e])=>t.localeCompare(e)).map(([t,e])=>`${encodeURIComponent(t)}=${encodeURIComponent(e)}`).join("&").replace(/%2C/g,",");import h,{useEffect as H,useRef as L,useState as O}from"react";var w=({as:n,preview:t,...e})=>{let[r,o]=O(!1),a=L(null),s=()=>{o(!0)};H(()=>{a.current?.complete&&s()});let c=n||"img";return h.createElement(h.Fragment,null,!r&&h.createElement(c,{alt:e.alt,className:e.className,"data-lqip":!0,height:e.height,id:e.id,src:t,style:e.style,width:e.width}),h.createElement(c,{"data-loading":r?null:!0,onLoad:s,ref:a,style:r?void 0:{height:"10px !important",opacity:0,pointerEvents:"none",position:"absolute",userSelect:"none",width:"10px !important",zIndex:-10},...e}))};import W from"react";var U=({as:n,baseUrl:t,projectId:e,dataset:r,id:o,hotspot:a,crop:s,width:c,height:p,mode:l="contain",preview:d,htmlWidth:m,htmlHeight:i,htmlId:u,queryParams:f,...y})=>{if(!o)throw new Error("Missing required `id` prop for <SanityImage>.");if(!t&&(!e||!r))throw new Error("Missing required `baseUrl` or `projectId` and `dataset` props for <SanityImage>.");t=t??`https://cdn.sanity.io/images/${e}/${r}/`;let M=o.endsWith("-svg"),$=d&&!M?w:n??"img",g={alt:y.alt??"",loading:y.loading??"lazy",id:u,...y};if(M)return W.createElement($,{...D({id:o,baseUrl:t}),...g});let C={baseUrl:t,id:o,crop:s,hotspot:a,width:c,height:p,mode:l,queryParams:f},{src:Q,...E}=P(C);return g.srcSet=S(C).join(", "),g.src=Q,g.width=m??E.width,g.height=i??E.height,d&&(g.as=n??"img",g.preview=d),W.createElement($,{...g})};export{w as ImageWithPreview,U as SanityImage,P as buildSrc,S as buildSrcSet,I as parseImageId};
2
2
  //# sourceMappingURL=index.js.map