@decocms/apps 0.21.4 → 0.22.0
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/commerce/components/Image.tsx +69 -83
- package/commerce/components/Picture.tsx +114 -0
- package/package.json +2 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ImgHTMLAttributes } from "react";
|
|
2
|
+
import { forwardRef } from "react";
|
|
2
3
|
|
|
3
4
|
// -------------------------------------------------------------------------
|
|
4
5
|
// Known asset prefixes that get stripped to produce a relative src path
|
|
@@ -34,7 +35,7 @@ export function getImageCdnDomain(): string {
|
|
|
34
35
|
// Fit options & optimization types
|
|
35
36
|
// -------------------------------------------------------------------------
|
|
36
37
|
|
|
37
|
-
export type FitOptions = "contain" | "cover";
|
|
38
|
+
export type FitOptions = "contain" | "cover" | "fill";
|
|
38
39
|
|
|
39
40
|
export const FACTORS = [1, 2];
|
|
40
41
|
|
|
@@ -163,92 +164,77 @@ export interface ImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, "s
|
|
|
163
164
|
height?: number;
|
|
164
165
|
/** @description Object-fit */
|
|
165
166
|
fit?: FitOptions;
|
|
166
|
-
/**
|
|
167
|
+
/**
|
|
168
|
+
* @description Web Vitals (LCP). Injects a `<link rel="preload">` tag
|
|
169
|
+
* alongside the `<img>`, sets `fetchPriority="high"` and `loading="eager"`.
|
|
170
|
+
* Use once per page for the LCP image.
|
|
171
|
+
*/
|
|
167
172
|
preload?: boolean;
|
|
173
|
+
/** @description Media query for responsive preloading (e.g. "(min-width: 768px)") */
|
|
174
|
+
media?: string;
|
|
168
175
|
}
|
|
169
176
|
|
|
170
|
-
export
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
src={src}
|
|
192
|
-
srcSet={srcSet}
|
|
193
|
-
sizes={srcSet ? (sizes ?? "(max-width: 768px) 100vw, 50vw") : undefined}
|
|
194
|
-
width={width}
|
|
195
|
-
height={height}
|
|
196
|
-
loading={loading ?? (preload ? "eager" : "lazy")}
|
|
197
|
-
decoding={decoding ?? "async"}
|
|
198
|
-
fetchPriority={preload ? "high" : undefined}
|
|
199
|
-
/>
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// -------------------------------------------------------------------------
|
|
204
|
-
// Picture (responsive art direction)
|
|
205
|
-
// -------------------------------------------------------------------------
|
|
206
|
-
|
|
207
|
-
export interface PictureSourceProps {
|
|
208
|
-
src: string;
|
|
209
|
-
width: number;
|
|
210
|
-
height?: number;
|
|
211
|
-
media: string;
|
|
212
|
-
fit?: FitOptions;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
export interface PictureProps extends Omit<ImageProps, "sizes"> {
|
|
216
|
-
sources: PictureSourceProps[];
|
|
217
|
-
}
|
|
177
|
+
export const Image = forwardRef<HTMLImageElement, ImageProps>(
|
|
178
|
+
function Image(
|
|
179
|
+
{
|
|
180
|
+
src,
|
|
181
|
+
width,
|
|
182
|
+
height,
|
|
183
|
+
fit = "cover",
|
|
184
|
+
preload,
|
|
185
|
+
media,
|
|
186
|
+
loading,
|
|
187
|
+
decoding,
|
|
188
|
+
srcSet: srcSetProp,
|
|
189
|
+
sizes,
|
|
190
|
+
fetchPriority,
|
|
191
|
+
...rest
|
|
192
|
+
},
|
|
193
|
+
ref,
|
|
194
|
+
) {
|
|
195
|
+
if (!height && typeof process !== "undefined") {
|
|
196
|
+
console.warn(`Missing height. This image will NOT be optimized: ${src}`);
|
|
197
|
+
}
|
|
218
198
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
<
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
199
|
+
const optimizedSrc = getOptimizedMediaUrl({
|
|
200
|
+
originalSrc: src,
|
|
201
|
+
width,
|
|
202
|
+
height,
|
|
203
|
+
fit,
|
|
204
|
+
});
|
|
205
|
+
const srcSet = srcSetProp ?? getSrcSet(src, width, height, fit);
|
|
206
|
+
const resolvedSizes = srcSet
|
|
207
|
+
? (sizes ?? "(max-width: 768px) 100vw, 50vw")
|
|
208
|
+
: undefined;
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<>
|
|
212
|
+
{preload && (
|
|
213
|
+
<link
|
|
214
|
+
as="image"
|
|
215
|
+
rel="preload"
|
|
216
|
+
href={optimizedSrc}
|
|
217
|
+
imageSrcSet={srcSet}
|
|
218
|
+
imageSizes={resolvedSizes}
|
|
219
|
+
fetchPriority={fetchPriority ?? "high"}
|
|
220
|
+
media={media}
|
|
239
221
|
/>
|
|
240
|
-
)
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
222
|
+
)}
|
|
223
|
+
<img
|
|
224
|
+
{...rest}
|
|
225
|
+
src={optimizedSrc}
|
|
226
|
+
srcSet={srcSet}
|
|
227
|
+
sizes={resolvedSizes}
|
|
228
|
+
width={width}
|
|
229
|
+
height={height}
|
|
230
|
+
loading={loading ?? (preload ? "eager" : "lazy")}
|
|
231
|
+
decoding={decoding ?? "async"}
|
|
232
|
+
fetchPriority={preload ? "high" : fetchPriority}
|
|
233
|
+
ref={ref}
|
|
234
|
+
/>
|
|
235
|
+
</>
|
|
236
|
+
);
|
|
237
|
+
},
|
|
238
|
+
);
|
|
253
239
|
|
|
254
240
|
export default Image;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
forwardRef,
|
|
4
|
+
useContext,
|
|
5
|
+
useMemo,
|
|
6
|
+
type ComponentPropsWithoutRef,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
} from "react";
|
|
9
|
+
import { getOptimizedMediaUrl, getSrcSet, type FitOptions } from "./Image";
|
|
10
|
+
|
|
11
|
+
// -------------------------------------------------------------------------
|
|
12
|
+
// Preload context — flows from <Picture preload> to child <Source> elements
|
|
13
|
+
// so each source can inject its own <link rel="preload"> with the correct
|
|
14
|
+
// media query for responsive art direction.
|
|
15
|
+
// -------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
interface PreloadContextValue {
|
|
18
|
+
preload: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const PreloadContext = createContext<PreloadContextValue>({ preload: false });
|
|
22
|
+
|
|
23
|
+
// -------------------------------------------------------------------------
|
|
24
|
+
// Source — composable <source> with automatic srcSet optimization and
|
|
25
|
+
// preload link injection when inside a <Picture preload>.
|
|
26
|
+
// -------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
export type SourceProps = Omit<
|
|
29
|
+
ComponentPropsWithoutRef<"source">,
|
|
30
|
+
"width" | "height"
|
|
31
|
+
> & {
|
|
32
|
+
src: string;
|
|
33
|
+
/** @description Improves Web Vitals (CLS|LCP) */
|
|
34
|
+
width: number;
|
|
35
|
+
/** @description Improves Web Vitals (CLS|LCP) */
|
|
36
|
+
height?: number;
|
|
37
|
+
/** @description Improves Web Vitals (LCP). Use high for LCP image. */
|
|
38
|
+
fetchPriority?: "high" | "low" | "auto";
|
|
39
|
+
/** @description Object-fit */
|
|
40
|
+
fit?: FitOptions;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const Source = forwardRef<HTMLSourceElement, SourceProps>(
|
|
44
|
+
function Source(
|
|
45
|
+
{ src, width, height, fetchPriority, fit = "cover", ...rest },
|
|
46
|
+
ref,
|
|
47
|
+
) {
|
|
48
|
+
const { preload } = useContext(PreloadContext);
|
|
49
|
+
|
|
50
|
+
const optimizedSrc = getOptimizedMediaUrl({
|
|
51
|
+
originalSrc: src,
|
|
52
|
+
width,
|
|
53
|
+
height,
|
|
54
|
+
fit,
|
|
55
|
+
});
|
|
56
|
+
const srcSet = rest.srcSet ?? getSrcSet(src, width, height, fit);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<>
|
|
60
|
+
{preload && (
|
|
61
|
+
<link
|
|
62
|
+
as="image"
|
|
63
|
+
rel="preload"
|
|
64
|
+
href={optimizedSrc}
|
|
65
|
+
imageSrcSet={srcSet}
|
|
66
|
+
fetchPriority={fetchPriority ?? "high"}
|
|
67
|
+
media={rest.media}
|
|
68
|
+
/>
|
|
69
|
+
)}
|
|
70
|
+
<source
|
|
71
|
+
{...rest}
|
|
72
|
+
srcSet={srcSet ?? optimizedSrc}
|
|
73
|
+
width={width}
|
|
74
|
+
height={height}
|
|
75
|
+
ref={ref}
|
|
76
|
+
/>
|
|
77
|
+
</>
|
|
78
|
+
);
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// -------------------------------------------------------------------------
|
|
83
|
+
// Picture — composable wrapper that provides preload context to children.
|
|
84
|
+
//
|
|
85
|
+
// Usage:
|
|
86
|
+
// <Picture preload={isLcp}>
|
|
87
|
+
// <Source media="(max-width: 767px)" src={mobile} width={320} height={280} />
|
|
88
|
+
// <Source media="(min-width: 768px)" src={desktop} width={1280} height={280} />
|
|
89
|
+
// <Image src={desktop} width={1280} height={280} />
|
|
90
|
+
// </Picture>
|
|
91
|
+
// -------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
export type PictureProps = ComponentPropsWithoutRef<"picture"> & {
|
|
94
|
+
children: ReactNode;
|
|
95
|
+
/**
|
|
96
|
+
* @description When true, child <Source> and <Image> elements inject
|
|
97
|
+
* `<link rel="preload">` tags for their respective media queries.
|
|
98
|
+
*/
|
|
99
|
+
preload?: boolean;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const Picture = forwardRef<HTMLPictureElement, PictureProps>(
|
|
103
|
+
function Picture({ children, preload = false, ...props }, ref) {
|
|
104
|
+
const value = useMemo(() => ({ preload }), [preload]);
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<PreloadContext.Provider value={value}>
|
|
108
|
+
<picture {...props} ref={ref}>
|
|
109
|
+
{children}
|
|
110
|
+
</picture>
|
|
111
|
+
</PreloadContext.Provider>
|
|
112
|
+
);
|
|
113
|
+
},
|
|
114
|
+
);
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/apps",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deco commerce apps for TanStack Start - Shopify, VTEX, commerce types, analytics utils",
|
|
6
6
|
"exports": {
|
|
7
7
|
"./commerce/types": "./commerce/types/commerce.ts",
|
|
8
8
|
"./commerce/components/JsonLd": "./commerce/components/JsonLd.tsx",
|
|
9
9
|
"./commerce/components/Image": "./commerce/components/Image.tsx",
|
|
10
|
+
"./commerce/components/Picture": "./commerce/components/Picture.tsx",
|
|
10
11
|
"./commerce/utils/*": "./commerce/utils/*.ts",
|
|
11
12
|
"./commerce/sdk/*": "./commerce/sdk/*.ts",
|
|
12
13
|
"./shopify": "./shopify/index.ts",
|