-
-
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] } |
@TheThirdRace could you upload the helper/Lifecycle file as well?
@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 thank you for the update.
I obviously had the problem of setting the width and height.