From 263686394799cc7eff737cae0f3800f979cf5ace Mon Sep 17 00:00:00 2001 From: Mateusz Bartosik Date: Thu, 12 Feb 2026 15:55:55 +0100 Subject: [PATCH 1/2] RDoc-3408 Lazy image loading --- src/components/Common/CardImage.tsx | 38 +++-------- src/components/Common/CardWithImage.tsx | 5 +- src/components/Common/LazyImage.tsx | 88 +++++++++++++++++++++++++ src/css/custom.css | 41 ++++++++++++ src/theme/MDXComponents/MDXImg.tsx | 6 ++ src/theme/MDXComponents/index.tsx | 7 ++ 6 files changed, 152 insertions(+), 33 deletions(-) create mode 100644 src/components/Common/LazyImage.tsx create mode 100644 src/theme/MDXComponents/MDXImg.tsx create mode 100644 src/theme/MDXComponents/index.tsx diff --git a/src/components/Common/CardImage.tsx b/src/components/Common/CardImage.tsx index 98557c7c35..5ed36dc9da 100644 --- a/src/components/Common/CardImage.tsx +++ b/src/components/Common/CardImage.tsx @@ -1,38 +1,16 @@ import React from "react"; -import ThemedImage from "@theme/ThemedImage"; +import LazyImage from "./LazyImage"; export interface CardImageProps { - imgSrc: string | { light: string; dark: string }; - imgAlt?: string; - className?: string; + imgSrc: string | { light: string; dark: string }; + imgAlt?: string; + className?: string; } export default function CardImage({ - imgSrc, - imgAlt = "", - className = "", + imgSrc, + imgAlt = "", + className = "", }: CardImageProps) { - const isThemedImage = typeof imgSrc === "object" && imgSrc !== null; - - if (isThemedImage) { - return ( - - ); - } - - return ( - {imgAlt} - ); + return ; } - diff --git a/src/components/Common/CardWithImage.tsx b/src/components/Common/CardWithImage.tsx index 838980d6e4..017dac575f 100644 --- a/src/components/Common/CardWithImage.tsx +++ b/src/components/Common/CardWithImage.tsx @@ -65,9 +65,8 @@ export default function CardWithImage({ "flex items-center justify-center", "rounded-xl mb-4 overflow-hidden", "relative aspect-[79/24]", - hasImage - ? "bg-black/40" - : "bg-gradient-to-b from-[#204879] to-[#0F1425] to-[70%]", + !hasImage && + "bg-gradient-to-b from-[#204879] to-[#0F1425] to-[70%]", )} > {hasImage ? ( diff --git a/src/components/Common/LazyImage.tsx b/src/components/Common/LazyImage.tsx new file mode 100644 index 0000000000..ba6e510b1c --- /dev/null +++ b/src/components/Common/LazyImage.tsx @@ -0,0 +1,88 @@ +import React, { useState } from "react"; +import clsx from "clsx"; +import ThemedImage from "@theme/ThemedImage"; + +export interface LazyImageProps + extends React.ImgHTMLAttributes { + imgSrc?: string | { light: string; dark: string }; +} + +export default function LazyImage({ + imgSrc, + src, + alt = "", + className, + style, + ...props +}: LazyImageProps) { + const [isLoaded, setIsLoaded] = useState(false); + const resolveSrc = (s: any) => { + if (typeof s === "object" && s !== null && "default" in s) { + return s.default; + } + return s; + }; + const finalSrc = imgSrc || src; + const isThemedImage = + typeof finalSrc === "object" && + finalSrc !== null && + !("default" in finalSrc) && + ("light" in finalSrc || "dark" in finalSrc); + const handleLoad = () => { + setIsLoaded(true); + }; + + return ( + + {!isLoaded && ( + + ); +} diff --git a/src/css/custom.css b/src/css/custom.css index 891f0c1143..0baacf1210 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -2,6 +2,14 @@ @custom-variant dark (&:is([data-theme="dark"] *)); +@media (prefers-reduced-motion: reduce) { + * { + animation: none !important; + transition: none !important; + scroll-behavior: auto !important; + } +} + /* Custom animations for layout switcher */ @keyframes slide-in-from-bottom { from { @@ -575,3 +583,36 @@ a { .theme-doc-sidebar-item-category-level-1 > .menu__list { @apply !m-0 !p-0 !border-0; } + +/* Skeleton loading animation */ +@keyframes skeleton-loading { + 0% { + background-position: 150% 0; + } + 100% { + background-position: -150% 0; + } +} + +.skeleton { + @apply !bg-black/10 dark:!bg-white/5; + color: transparent !important; + background-image: linear-gradient( + 45deg, + color-mix(in oklab, var(--color-black) 7.5%, transparent) 40%, + color-mix(in oklab, var(--color-black) 0.5%, transparent) 50%, + color-mix(in oklab, var(--color-black) 7.5%, transparent) 60% + ); + background-size: 350% 100%; + border-radius: var(--radius-xl); + animation: skeleton-loading 2000ms infinite linear; +} + +[data-theme="dark"] .skeleton { + background-image: linear-gradient( + 45deg, + color-mix(in oklab, var(--color-white) 0.5%, transparent) 40%, + color-mix(in oklab, var(--color-white) 7.5%, transparent) 50%, + color-mix(in oklab, var(--color-white) 0.5%, transparent) 60% + ); +} diff --git a/src/theme/MDXComponents/MDXImg.tsx b/src/theme/MDXComponents/MDXImg.tsx new file mode 100644 index 0000000000..e45d01f361 --- /dev/null +++ b/src/theme/MDXComponents/MDXImg.tsx @@ -0,0 +1,6 @@ +import React from "react"; +import LazyImage from "@site/src/components/Common/LazyImage"; + +export default function MDXImg(props: any) { + return ; +} diff --git a/src/theme/MDXComponents/index.tsx b/src/theme/MDXComponents/index.tsx new file mode 100644 index 0000000000..8c5f1a88bf --- /dev/null +++ b/src/theme/MDXComponents/index.tsx @@ -0,0 +1,7 @@ +import MDXComponents from "@theme-original/MDXComponents"; +import MDXImg from "./MDXImg"; + +export default { + ...MDXComponents, + img: MDXImg, +}; From 2bc021b120b8288e556635c06c04790080f8b6f0 Mon Sep 17 00:00:00 2001 From: Damian Olszewski Date: Fri, 13 Feb 2026 10:57:31 +0100 Subject: [PATCH 2/2] RDoc-3408 Cleanup --- eslint.config.js | 1 + src/components/Common/CardImage.tsx | 16 ---- src/components/Common/CardWithImage.tsx | 6 +- .../Common/CardWithImageHorizontal.tsx | 6 +- src/components/Common/LazyImage.tsx | 87 +++++++++---------- src/theme/MDXComponents/MDXImg.tsx | 4 +- src/theme/MDXComponents/index.tsx | 2 +- 7 files changed, 49 insertions(+), 73 deletions(-) delete mode 100644 src/components/Common/CardImage.tsx diff --git a/eslint.config.js b/eslint.config.js index 68b0b81ed9..bb24d09026 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -38,6 +38,7 @@ module.exports = [ HTMLButtonElement: "readonly", HTMLDivElement: "readonly", HTMLLIElement: "readonly", + HTMLImageElement: "readonly", MouseEvent: "readonly", KeyboardEvent: "readonly", Node: "readonly", diff --git a/src/components/Common/CardImage.tsx b/src/components/Common/CardImage.tsx deleted file mode 100644 index 5ed36dc9da..0000000000 --- a/src/components/Common/CardImage.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from "react"; -import LazyImage from "./LazyImage"; - -export interface CardImageProps { - imgSrc: string | { light: string; dark: string }; - imgAlt?: string; - className?: string; -} - -export default function CardImage({ - imgSrc, - imgAlt = "", - className = "", -}: CardImageProps) { - return ; -} diff --git a/src/components/Common/CardWithImage.tsx b/src/components/Common/CardWithImage.tsx index 017dac575f..5c1dfb89e7 100644 --- a/src/components/Common/CardWithImage.tsx +++ b/src/components/Common/CardWithImage.tsx @@ -4,7 +4,7 @@ import Heading from "@theme/Heading"; import { Icon } from "@site/src/components/Common/Icon"; import { IconName } from "@site/src/typescript/iconName"; import Badge from "@site/src/components/Common/Badge"; -import CardImage from "@site/src/components/Common/CardImage"; +import LazyImage from "@site/src/components/Common/LazyImage"; import isInternalUrl from "@docusaurus/isInternalUrl"; import clsx from "clsx"; import { useTagLimit } from "@site/src/hooks/useTagLimit"; @@ -70,9 +70,9 @@ export default function CardWithImage({ )} > {hasImage ? ( - - { @@ -16,22 +16,13 @@ export default function LazyImage({ ...props }: LazyImageProps) { const [isLoaded, setIsLoaded] = useState(false); - const resolveSrc = (s: any) => { - if (typeof s === "object" && s !== null && "default" in s) { - return s.default; - } - return s; - }; - const finalSrc = imgSrc || src; - const isThemedImage = - typeof finalSrc === "object" && - finalSrc !== null && - !("default" in finalSrc) && - ("light" in finalSrc || "dark" in finalSrc); - const handleLoad = () => { + + const handleLoaded = () => { setIsLoaded(true); }; + const sources = getSources({ imgSrc, src }); + return ( ); } + +function getSources({ + imgSrc, + src, +}: Pick): ThemedImageProps["sources"] { + if (src) { + return { + light: src, + dark: src, + }; + } + + if (typeof imgSrc === "string") { + return { + light: imgSrc, + dark: imgSrc, + }; + } + + return imgSrc; +} diff --git a/src/theme/MDXComponents/MDXImg.tsx b/src/theme/MDXComponents/MDXImg.tsx index e45d01f361..01537fb691 100644 --- a/src/theme/MDXComponents/MDXImg.tsx +++ b/src/theme/MDXComponents/MDXImg.tsx @@ -1,6 +1,6 @@ -import React from "react"; +import React, { ComponentProps } from "react"; import LazyImage from "@site/src/components/Common/LazyImage"; -export default function MDXImg(props: any) { +export default function MDXImg(props: ComponentProps) { return ; } diff --git a/src/theme/MDXComponents/index.tsx b/src/theme/MDXComponents/index.tsx index 8c5f1a88bf..99f4863d68 100644 --- a/src/theme/MDXComponents/index.tsx +++ b/src/theme/MDXComponents/index.tsx @@ -4,4 +4,4 @@ import MDXImg from "./MDXImg"; export default { ...MDXComponents, img: MDXImg, -}; +} satisfies typeof MDXComponents;