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 98557c7c35..0000000000 --- a/src/components/Common/CardImage.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from "react"; -import ThemedImage from "@theme/ThemedImage"; - -export interface CardImageProps { - imgSrc: string | { light: string; dark: string }; - imgAlt?: string; - className?: string; -} - -export default function CardImage({ - imgSrc, - imgAlt = "", - className = "", -}: CardImageProps) { - const isThemedImage = typeof imgSrc === "object" && imgSrc !== null; - - if (isThemedImage) { - return ( - - ); - } - - return ( - {imgAlt} - ); -} - diff --git a/src/components/Common/CardWithImage.tsx b/src/components/Common/CardWithImage.tsx index 838980d6e4..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"; @@ -65,15 +65,14 @@ 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 ? ( - - { + imgSrc?: string | { light: string; dark: string }; +} + +export default function LazyImage({ + imgSrc, + src, + alt = "", + className, + style, + ...props +}: LazyImageProps) { + const [isLoaded, setIsLoaded] = useState(false); + + const handleLoaded = () => { + setIsLoaded(true); + }; + + const sources = getSources({ imgSrc, src }); + + return ( + + {!isLoaded && ( + + ); +} + +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/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..01537fb691 --- /dev/null +++ b/src/theme/MDXComponents/MDXImg.tsx @@ -0,0 +1,6 @@ +import React, { ComponentProps } from "react"; +import LazyImage from "@site/src/components/Common/LazyImage"; + +export default function MDXImg(props: ComponentProps) { + return ; +} diff --git a/src/theme/MDXComponents/index.tsx b/src/theme/MDXComponents/index.tsx new file mode 100644 index 0000000000..99f4863d68 --- /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, +} satisfies typeof MDXComponents;