-
-
Save TheThirdRace/7f270a786629f119b57d1b2227a4b113 to your computer and use it in GitHub Desktop.
/** | |
* # `<Image>` | |
* | |
* This component is a merge between `next/image` and `Chakra-ui`. | |
* - last updated on 2023-08-08 with `next/image` 13.4.13 and `chakra-ui/react` 2.8.0 | |
* - https://github.com/vercel/next.js/blob/v13.4.13/packages/next/src/client/image-component.tsx | |
* - https://github.com/vercel/next.js/blob/canary/packages/next/src/client/image-component.tsx | |
* - https://github.com/vercel/next.js/commits/canary/packages/next/src/client/image-component.tsx | |
* - https://github.com/vercel/next.js/compare/v13.4.4...canary | |
* | |
* Associated `gist`: <https://gist.github.com/TheThirdRace/7f270a786629f119b57d1b2227a4b113> | |
* | |
* ## Pros | |
* | |
* - Use NextJs backend solution so you get `static` or `on the fly` image optimization | |
* - Offer the same optimizations as `next/image` (lazy loading, priority, async decoding, no CLS, blur placeholder, etc.) | |
* - Use Chakra's theme (`variant`) so you have full control of styling | |
* - `<img>` is back to `display: inline-block` by default | |
* - Forward ref to `<img>` | |
* - No more fiddling with `onLoadComplete` callback from `next/image` | |
* - You can determine when an image is completely loaded | |
* - You can pass a callback `ref` and check if `data-loaded` is `true` | |
* - You can use `css` to target `[data-loaded=true]` | |
* - All icons are served automatically through the `1x` or `2x` pixel density optimization | |
* - Passing `sizesMax={0}` can force a bigger image to be served in the `1x` and `2x` mode | |
* - All images are served automatically through an `srcset` auto-build function | |
* - Load configs through `NextJs` config | |
* - No more fiddling trying to build a `sizes` manually | |
* - Simply pass `sizesMax={ImageMaxWidth}` or don't pass `sizesMax` at all (defaults to highest possible value) | |
* - `sizesMax` allows you to limit image width according to your design, not the viewport | |
* - No more loading a 3840px wide image on a 4K screen when your `main` section is 1200px | |
* - Use semantic HTML tags | |
* - `<img>` is used for the image | |
* - `<picture>` is used for the wrapper/container (optional) | |
* - `height` & `width` are extremely recommended, but not mandatory | |
* - Can use a blurry placeholder for better user experience and core vitals | |
* - Automatic when using static images (`import`) | |
* - You can manually pass a data uri for dynamic images | |
* - Low `height` and `width` images like icons won't apply the blurry placeholder as it lower performance | |
* - `loader` function allow to build the final `src` url, so you can support many image providers | |
* - Possible to use with a **secure** `Content-Security-Policy` header | |
* - Extra performance by using `content-visibility: auto` on the `<picture>` wrapper | |
* - Not available by default on `<img>` to avoid scrolling up issues on Chrome | |
* - Could be added manually on `<img>` through styles if wanted | |
* - Smaller than `next/image` solution by almost 200 lines of code | |
* - Smaller by almost 450 lines of codes if you count all the extra messages from development (which are loaded in PROD) | |
* | |
* ## Cons | |
* | |
* - Doesn't support Chakra's inline styling (by personal choice, could easily be added) | |
* - Using a different `backgroundSize`/`backgroundPosition` from default requires to style the `blur` placeholder | |
* - Use native `loading=lazy`, meaning the feature isn't supported for all browsers yet | |
* - Automatic blurry placeholder generation only works when your source image is a avif, jpg, png or webp | |
* - Same restrictions as NextJs since the component use their image optimization solution | |
* - Be advised, the "source" image is not the image served to your users, it's the unoptimized image before optimization | |
* - Using `<img>` without it's wrapper (`<picture>`) will give a very low CLS instead of none (ex: 0.03) | |
* - Serving "responsive" images can increase data consumption, but this should be negligible because: | |
* - Images are optimized to a low size to begin with | |
* - Those most affected are users with big screens, which usually don't mind more data | |
* - Users don't resize their browser window non-stop | |
* | |
* ## Tips & Tricks | |
* | |
* ### Optimization | |
* | |
* - Pass `width` & `height` whenever you can, it's the biggest optimization you're gonna get out of the box | |
* - Use `import` method for your images, it improves your Core Web Vitals and the user experience | |
* | |
* ### `<picture>` wrapper | |
* | |
* - Will be added automatically under these circumstances | |
* - Pass `width` & `height` props | |
* - Define a style for Image's `layPicture` part in the theme | |
* - `<picture>` wrapper is mandatory to reach a cumulative layout shift (CLS) of 0 | |
* - This implementation will always have a CLS of 0, no matter if it's a newer or older browser | |
* - The new `next/image` in NextJS `13.x` won't have 0 CLS, it'll get close on newer browser, but older browsers will have huge CLS | |
* - You won't be penalized by Google ranking as long as you keep CLS < 0.1, which makes the wrapper "optional" | |
* | |
* ### `sizesMax` | |
* | |
* - Pass `sizesMax={0}` to force an image to be optimized with `srcset` containing `1x, 2x` variants | |
* - Mostly for icons, but you could use this for normal images too | |
* - Don't pass `sizesMax` to force an image to be optimized for the current viewport width | |
* - If an image is less than the full screen's width, you can pass its max size like this `sizesMax={992}` | |
*/ | |
import { chakra } from '@chakra-ui/react' | |
import Head from 'next/head' | |
import { type ImageProps as NextImageProps, type StaticImageData } from 'next/image' | |
import { forwardRef, useImperativeHandle, useState, type Dispatch, type ReactElement, type SetStateAction } from 'react' | |
import { | |
defaultLoader, | |
useImageAttributes, | |
useImageOnLoad, | |
useImageStyle, | |
type GenerateImageAttributesReturn, | |
type ImageProps | |
} from '~/helper/Image' | |
import { Rename } from '~/shared/type/Typescript' | |
/** ******************************************************************************************************************* | |
* Types | |
*/ | |
type ImageNativeProps = Partial<Pick<HTMLImageElement, 'alt'>> & | |
Partial<Rename<Pick<HTMLImageElement, 'height'>, 'height', 'htmlHeight'>> & | |
Partial<Rename<Pick<HTMLImageElement, 'width'>, 'width', 'htmlWidth'>> & { | |
'data-set-load-state': Dispatch<SetStateAction<boolean>> | |
} | |
type ImagePriorityProps = Pick<NextImageProps, 'crossOrigin' | 'priority'> & | |
Pick<GenerateImageAttributesReturn, 'sizes' | 'src' | 'srcset'> | |
type StaticImageProps = Pick<StaticImageData, 'height' | 'src' | 'width'> & Pick<StaticImageData, 'blurDataURL'> | |
/** ******************************************************************************************************************* | |
* * Components * | |
*/ | |
const ImageNative = forwardRef<HTMLImageElement, ImageNativeProps>( | |
({ alt, htmlWidth, htmlHeight, 'data-set-load-state': setLoadState, ...chakraInternals }: ImageNativeProps, ref) => { | |
// Handle refs to the same element | |
// 1. `imgRef` => from `useRef` and is used to link `ref` with `callbackRef` (link between internal and external refs) | |
// 2. `ref` => from `forwardRef` and is used to give access to the internal ref from the parent | |
// 3. `callbackRef` => from `useCallback` and is used to set image loaded state even on static rendered pages | |
// | |
// Inspired by | |
// - https://medium.com/@teh_builder/ref-objects-inside-useeffect-hooks-eb7c15198780 | |
// - https://reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node | |
const { callbackRef, imgRef } = useImageOnLoad({ setLoadState }) | |
useImperativeHandle<HTMLImageElement | null, HTMLImageElement | null>(ref, () => imgRef.current) | |
return ( | |
// eslint-disable-next-line @next/next/no-img-element | |
<img | |
alt={alt} | |
height={htmlHeight} | |
ref={callbackRef} // ? use callback ref to catch when it updates | |
width={htmlWidth} | |
// eslint-disable-next-line react/jsx-props-no-spreading | |
{...chakraInternals} | |
/> | |
) | |
} | |
) | |
const ImagePriority = ({ crossOrigin, sizes, src, srcset }: ImagePriorityProps): ReactElement => { | |
return ( | |
// Note how we omit the `href` attribute, as it would only be relevant | |
// for browsers that do not support `imagesrcset`, and in those cases | |
// it would likely cause the incorrect image to be preloaded. | |
// | |
// https://html.spec.whatwg.org/multipage/semantics.html#attr-link-imagesrcset | |
<Head> | |
<link | |
as='image' | |
crossOrigin={crossOrigin} | |
fetchpriority='high' // eslint-disable-line react/no-unknown-property | |
href={srcset ? undefined : src} | |
imageSizes={sizes} | |
imageSrcSet={srcset} | |
key={`__nimg-${src}${srcset}${sizes}`} | |
rel='preload' | |
/> | |
</Head> | |
) | |
} | |
export const Image = forwardRef<HTMLImageElement, ImageProps>( | |
( | |
{ | |
alt, | |
blurDataURL: paramBlurDataURL, | |
crossOrigin, | |
height: paramHeight, | |
loader = defaultLoader, | |
priority = false, | |
quality, | |
sizesMax, | |
src: paramSrc, | |
sx, | |
title, | |
variant, | |
width: paramWidth, | |
...chakraInternals | |
}: ImageProps, | |
ref | |
): ReactElement => { | |
// Manage values according to image mode: Static or Dynamic | |
const { blurDataURL, height, src, width } = | |
typeof paramSrc === 'string' | |
? { | |
blurDataURL: paramBlurDataURL, | |
height: paramHeight, | |
src: paramSrc, | |
width: paramWidth | |
} | |
: ({ | |
...paramSrc, | |
...(paramHeight ? { height: paramHeight } : {}), | |
...(paramWidth ? { width: paramWidth } : {}) | |
} as StaticImageProps) | |
// Keep trace of when the image is loaded | |
const [imgLoaded, setImgLoaded] = useState(false) | |
// Retrieve styling | |
const { styles, withWrapper } = useImageStyle({ blurDataURL, height, imgLoaded, src, variant, width }) | |
// Retrieve image attributes | |
const { | |
src: imgSrc, | |
srcset: imgSrcSet, | |
sizes: imgSizes | |
} = useImageAttributes({ | |
loader, | |
quality, | |
sizesMax, | |
src, | |
width | |
}) | |
// Image component | |
const imgProps = { | |
as: ImageNative, | |
alt, | |
decoding: 'async' as const, | |
...(priority ? { fetchpriority: 'high' } : { loading: 'lazy' as const }), | |
htmlHeight: height, | |
htmlWidth: width, | |
'data-set-load-state': setImgLoaded, | |
'data-loaded': imgLoaded, | |
ref, | |
// ? `src` must be the last parameter within those 3 | |
// ? Safari has a bug that would download the image in `src` before `sizes` and `srcset` | |
// ? are set and then download a second image when both are set. | |
// ? | |
// ? By putting `src` in last position, Safari won't initiate a download until `src` is | |
// ? updated in the DOM correctly, | |
sizes: imgSizes, | |
srcSet: imgSrcSet, | |
src: imgSrc, | |
// ? --------------------------------------------------------------------------------------, | |
sx: styles.image, | |
title, | |
// eslint-disable-next-line react/jsx-props-no-spreading, | |
...chakraInternals | |
} | |
const img = ( | |
<> | |
<chakra.img | |
// eslint-disable-next-line react/jsx-props-no-spreading | |
{...imgProps} | |
/> | |
<noscript> | |
<chakra.img | |
// eslint-disable-next-line react/jsx-props-no-spreading | |
{...imgProps} | |
sx={styles.imageNoScript} | |
/> | |
</noscript> | |
</> | |
) | |
// Add a `<picture>` wrapper if required | |
const image = withWrapper ? <chakra.picture sx={{ ...styles.picture, ...sx }}>{img}</chakra.picture> : img | |
return ( | |
<> | |
{image} | |
{priority ? ( | |
<ImagePriority crossOrigin={crossOrigin} sizes={imgSizes} src={imgSrc} srcset={imgSrcSet} /> | |
) : undefined} | |
</> | |
) | |
} | |
) |
import { useMultiStyleConfig, type ChakraProps, type SystemStyleObject, type ThemingProps } from '@chakra-ui/react' | |
import { mergeWith } from '@chakra-ui/utils' | |
import { imageConfigDefault, type ImageConfigComplete } from 'next/dist/shared/lib/image-config' | |
import { type ImageLoaderProps, type ImageProps as NextImageProps } from 'next/image' | |
import { Dispatch, MutableRefObject, SetStateAction, useCallback, useMemo, useRef } from 'react' | |
/** ******************************************************************************************************************* | |
* Types | |
*/ | |
export type ImageProps = Pick<NextImageProps, 'blurDataURL' | 'crossOrigin' | 'priority' | 'src'> & | |
Partial<Pick<HTMLImageElement, 'alt' | 'height' | 'title' | 'width'>> & | |
Pick<ChakraProps, 'sx'> & | |
Pick<ThemingProps, 'variant'> & { | |
loader?: ImageLoaderWithConfig | |
quality?: number | |
sizesMax?: SizesMax | |
} | |
type GenerateCumulativeLayoutShiftFixProps = Pick<ImageProps, 'height' | 'sizesMax' | 'width'> | |
type GenerateImageAttributesProps = Required<Pick<ImageProps, 'loader'>> & | |
Pick<ImageProps, 'quality' | 'sizesMax' | 'width'> & | |
Pick<HTMLImageElement, 'src'> | |
export type GenerateImageAttributesReturn = Pick<HTMLImageElement, 'src'> & | |
Partial<Pick<HTMLImageElement, 'sizes' | 'srcset'>> | |
type ImageConfig = ImageConfigComplete & { allSizes: number[] } | |
type ImageLoaderWithConfig = (resolverProps: ImageLoaderPropsWithConfig) => string | |
type ImageLoaderPropsWithConfig = ImageLoaderProps & { | |
config: Readonly<ImageConfig> | |
} | |
type IsLayoutProps = Pick<ImageProps, 'sizesMax' | 'width'> | |
type UseImageOnLoadProps = { | |
setLoadState: Dispatch<SetStateAction<boolean>> | |
} | |
/** | |
* ! Makes sure `contentMaxWidthInPixel` from `page.ts` is included in `SizeMax` | |
* ! Makes sure values here are in sync with `next.config.js` | |
*/ | |
export type SizesMax = 0 | 320 | 480 | 640 | 750 | 828 | 992 | 1080 | 1200 | 1440 | 1920 | 2048 | 2560 | 3840 | |
type UseImageOnLoadReturn = { | |
callbackRef: (img: HTMLImageElement) => void | |
imgRef: MutableRefObject<HTMLImageElement | null> | |
} | |
type UseImageStyleProps = Pick<ImageProps, 'blurDataURL' | 'height' | 'sizesMax' | 'variant' | 'width'> & | |
Pick<HTMLImageElement, 'src'> & { | |
imgLoaded: boolean | |
} | |
type UseImageStyleReturn = { | |
styles: { | |
image: SystemStyleObject | |
imageNoScript: SystemStyleObject | |
picture: SystemStyleObject | |
} | |
withWrapper: boolean | |
} | |
/** ******************************************************************************************************************* | |
* * Image configurations * | |
* https://github.com/vercel/next.js/blob/canary/packages/next/next-server/server/image-config.ts | |
*/ | |
const defaultBlurDataURL = | |
// 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAIAAAACUFjqAAAAA3NCSVQICAjb4U/gAAAAIElEQVQYlWNUUlJiwA1Y/v37h0/6////FOgeSMMpshsAm54bX5qzRrgAAAAASUVORK5CYII=', | |
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNU+g8AAUkBI5mqlHIAAAAASUVORK5CYII=' | |
const defaultQuality = 75 | |
const tmpConfig: ImageConfigComplete = mergeWith( | |
{}, | |
imageConfigDefault, | |
process.env.__NEXT_IMAGE_OPTS as unknown as ImageConfigComplete | |
) | |
const imageConfig: ImageConfig = mergeWith({}, tmpConfig, { | |
allSizes: [...tmpConfig.imageSizes, ...tmpConfig.deviceSizes].sort((a, b) => a - b) | |
}) | |
const { allSizes: configAllSizes, deviceSizes: configDeviceSizes, imageSizes: configImageSizes } = imageConfig | |
/** ******************************************************************************************************************* | |
* * Functions * | |
*/ | |
export const defaultLoader = ({ config, src, width, quality = defaultQuality }: ImageLoaderPropsWithConfig): string => | |
src.endsWith('.svg') && !config.dangerouslyAllowSVG | |
? src | |
: `${config.path}?url=${encodeURIComponent(src)}&w=${width}&q=${quality}` | |
const isLayoutFixed = ({ sizesMax, width = configDeviceSizes[configDeviceSizes.length - 1] }: IsLayoutProps): boolean => | |
sizesMax === 0 || width < configDeviceSizes[0] || configImageSizes.includes(width) | |
const generateCumulativeLayoutShiftFix = ({ height, sizesMax, width }: GenerateCumulativeLayoutShiftFixProps) => { | |
let clsFix = {} | |
if (height && width) { | |
clsFix = { | |
aspectRatio: `${width}/${height}`, | |
...(isLayoutFixed({ sizesMax, width }) | |
? { | |
height: `${height}px`, | |
width: `${width}px` | |
} | |
: { | |
paddingBlockStart: `calc(${height} / ${width} * 100%)` | |
}) | |
} | |
} | |
return clsFix | |
} | |
export const useImageAttributes = ({ | |
loader, | |
quality, | |
sizesMax, | |
src, | |
width = configDeviceSizes[configDeviceSizes.length - 1] | |
}: GenerateImageAttributesProps): GenerateImageAttributesReturn => { | |
return useMemo(() => { | |
let imgAttributes: GenerateImageAttributesReturn | |
if (src && (src.startsWith('data:') || src.startsWith('blob:'))) { | |
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs | |
imgAttributes = { src, srcset: undefined, sizes: undefined } | |
} else if (isLayoutFixed({ sizesMax, width })) { | |
const widths = [ | |
...new Set( | |
/** | |
* This means that most OLED screens that say they are 3x resolution, are actually 3x in the green color, | |
* but only 1.5x in the red and blue colors. | |
* | |
* Showing a 3x resolution image in the app vs a 2x resolution image will be visually the same, though the | |
* 3x image takes significantly more data. Even true 3x resolution screens are wasteful as the human eye | |
* cannot see that level of detail without something like a magnifying glass. | |
* | |
* https://blog.twitter.com/engineering/en_us/topics/infrastructure/2019/capping-image-fidelity-on-ultra-high-resolution-devices.html | |
*/ | |
[width, width * 2].map( | |
(w) => configAllSizes.find((s) => s >= w) || configAllSizes[configAllSizes.length - 1] | |
) | |
) | |
] | |
imgAttributes = { | |
sizes: undefined, | |
src: loader({ config: imageConfig, src, quality, width: widths[1] }), | |
srcset: widths | |
.map((w, i) => `${loader({ config: imageConfig, src, quality, width: w })} ${i + 1}x`) | |
.join(', ') | |
} | |
} else { | |
const maxSizes = sizesMax || configDeviceSizes[configDeviceSizes.length - 1] | |
const widths = [...configDeviceSizes.filter((w) => w < maxSizes), maxSizes] | |
imgAttributes = { | |
sizes: widths | |
.map((w, i) => { | |
return i < widths.length - 1 ? ` (max-width: ${w}px) ${w}px` : ` ${w}px` | |
}) | |
.join(','), | |
src: loader({ config: imageConfig, src, quality, width: widths[widths.length - 1] }), | |
srcset: widths.map((w) => `${loader({ config: imageConfig, src, quality, width: w })} ${w}w`).join(', ') | |
} | |
} | |
return imgAttributes | |
}, [loader, quality, sizesMax, src, width]) | |
} | |
export const useImageOnLoad = ({ setLoadState }: UseImageOnLoadProps): UseImageOnLoadReturn => { | |
// Handle refs to the same element | |
// 1. `imgRef` => from `useRef` and is used to link `ref` with `callbackRef` (link between internal and external refs) | |
// 2. `ref` => from `forwardRef` and is used to give access to the internal ref from the parent | |
// 3. `callbackRef` => from `useCallback` and is used to set image loaded state even on static rendered pages | |
// | |
// Inspired by | |
// - https://medium.com/@teh_builder/ref-objects-inside-useeffect-hooks-eb7c15198780 | |
// - https://reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node | |
const imgRef = useRef<HTMLImageElement | null>(null) | |
const callbackRef = useCallback( | |
// ? Because the page could be static rendered, the image could already be loaded before React registers the image's `onload` event, meaning it would never fire | |
// ? That's why we use a `ref` handler instead, see https://stackoverflow.com/q/39777833/266535 | |
(img: HTMLImageElement) => { | |
// Check if a node is actually passed. Otherwise node would be null. | |
if (img) { | |
// You can now do what you need to, addEventListeners, measure, etc. | |
const handleLoad = () => { | |
if (!img.src.startsWith('data:') && !img.src.startsWith('blob:')) { | |
const p = 'decode' in img ? img.decode() : Promise.resolve() | |
p.catch(() => {}) | |
.then(() => setLoadState(true)) | |
.catch(() => {}) | |
} | |
} | |
if (img.complete) { | |
// ? If the real image fails to load, this will still remove the blurred image | |
handleLoad() | |
} else { | |
img.onload = handleLoad // eslint-disable-line no-param-reassign, unicorn/prefer-add-event-listener | |
} | |
} | |
imgRef.current = img // Save a reference to the node | |
}, | |
[setLoadState] | |
) | |
return { | |
callbackRef, | |
imgRef | |
} | |
} | |
export const useImageStyle = ({ | |
blurDataURL = defaultBlurDataURL, | |
height, | |
imgLoaded, | |
sizesMax, | |
src, | |
variant, | |
width | |
}: UseImageStyleProps): UseImageStyleReturn => { | |
// Retrieve styles from theme | |
const { layPicture, layPictureCls, layImage, layImageCls, layImageNoScript, preImage, preImageBlur, preImageCls } = | |
useMultiStyleConfig('Image', { variant }) | |
// Do we need a wrapper? | |
const withWrapperFromProps = !!(width && height) | |
const withWrapperFromTheme = !!(layPicture && layPicture.constructor === Object && Object.keys(layPicture).length) | |
// Do we need a blur placeholder? | |
const withBlurPlaceholder = !!( | |
!imgLoaded && | |
blurDataURL && | |
(!height || height > 48) && | |
(!width || width > 48) && | |
!src.startsWith('data:') && | |
!src.startsWith('blob:') | |
) | |
return { | |
styles: { | |
image: { | |
// Styles for `<img>` when used with `<picture>` wrapper | |
// if wrapper is activated by theme then `variant` can override styles from wrapper | |
// if wrapper is activated by props then styles from wrapper will override `variant` | |
...(withWrapperFromTheme ? { ...layImageCls, ...preImageCls } : {}), | |
...layImage, | |
...preImage, | |
...(withWrapperFromProps ? { ...layImageCls, ...preImageCls } : {}), | |
...(withBlurPlaceholder | |
? { | |
'--blurBackgroundImage': `url("${blurDataURL}")`, | |
...preImageBlur | |
} | |
: {}) | |
}, | |
imageNoScript: { | |
...layImageNoScript | |
}, | |
picture: { | |
...generateCumulativeLayoutShiftFix({ height, sizesMax, width }), | |
// Styles for `<picture>` wrapper | |
// if wrapper is activated by theme then `variant` can override styles from wrapper | |
// if wrapper is activated by props then styles from wrapper will override `variant` | |
...(withWrapperFromTheme ? layPictureCls : {}), | |
...layPicture, | |
...(withWrapperFromProps ? layPictureCls : {}) | |
} | |
}, | |
withWrapper: withWrapperFromProps || withWrapperFromTheme | |
} | |
} |
import { type ComponentMultiStyleConfig } from '@chakra-ui/react' | |
import { anatomy, type PartsStyleObject } from '@chakra-ui/theme-tools' | |
const partsAnatomy = anatomy('image').parts( | |
'layPicture', | |
'layPictureCls', | |
'layImage', | |
'layImageCls', | |
'layImageNoScript', | |
'preImage', | |
'preImageBlur' | |
) | |
export type ImageStyleObject = PartsStyleObject<typeof partsAnatomy> | |
export const Image: ComponentMultiStyleConfig = { | |
parts: [], | |
baseStyle: { | |
layPicture: {}, | |
layPictureCls: { | |
// layout | |
display: 'block', // necessary for firefox | |
position: 'relative', | |
// box model | |
boxSizing: 'border-box', | |
// misc | |
contentVisibility: 'auto', | |
overflow: 'hidden' | |
}, | |
layImage: { | |
// layout | |
display: 'inline-block' | |
}, | |
layImageCls: { | |
// layout | |
display: 'block', // necessary for firefox | |
inset: 0, | |
position: 'absolute' | |
}, | |
layImageNoScript: { | |
// layout | |
position: 'absolute', | |
top: 0 | |
}, | |
preImage: { | |
// box model | |
height: 'auto', | |
maxWidth: 'inherit', | |
width: 'auto' | |
// misc | |
// ! not activated because it cause jumpiness while scrolling up in Chrome | |
// contentVisibility: 'auto' | |
// containIntrinsicSize: 'width height' // obviously need to be adjusted | |
}, | |
preImageBlur: { | |
// visual | |
backgroundImage: 'var(--blurBackgroundImage)', | |
backgroundPosition: '0% 0%', | |
backgroundSize: 'cover', | |
filter: 'blur(1.25rem)' | |
}, | |
preImageCls: { | |
// box model | |
maxHeight: '100%', | |
maxWidth: '100%', | |
minHeight: '100%', | |
minWidth: '100%' | |
} | |
}, | |
variants: {}, | |
defaultProps: {} | |
} |
images: { | |
/** | |
* ! Highly suggested to have your max content width defined here (it will better optimize the image size) | |
* ! For example, on my website, an image is at most 992px which is the centered part of the viewport where I put content | |
* ! Makes sure values here are in sync with `helper/Image.ts` | |
*/ | |
deviceSizes: [320, 480, 640, 750, 828, 992, 1080, 1200, 1440, 1920, 2048, 2560, 3840], | |
domains: [], | |
formats: ['image/avif', 'image/webp'], | |
minimumCacheTTL: 86400 // if `no max-age` or `s-max-age` defined for an image, cache it `1 day` | |
}, |
// ex: Rename<NextLinkProps, 'as', 'asRoute'> | |
export type Rename<T, K extends keyof T, N extends string> = Pick<T, Exclude<keyof T, K>> & { [P in N]: T[K] } |
@ifxli Just updated the helper/Lifecycle.ts
for you.
Originally, everything was mostly in the same file. As I ported more and more features, I had to separate stuff in multiple files to keep it tidy. I also made a big refactor at some point, which created new files too...
So thanks for pointing out which files I was missing in the gist. It also gave me the nudge to update it with the latest version of Chakra and NextJs.
@TheThirdRace Any chance this will be released into the wild as it's own component?
@bline Yes, but not any time soon :(
Given my repo is private, it makes it very hard to share public packages. Or at least, I think it does... I don't have much experience in creating packages 😅
If I can find a quick and easy way to simply move all my components to a separate package without impacting my private repo, I would definitely proceed this way. The only reason I keep my repo private is for some proprietary content (business logic), I would gladly share all my components as a public library if I can and it doesn't give me headaches to manage.
@TheThirdRace could you upload the helper/Lifecycle file as well?