CoolFace
Apppublic

lerobot/robot-learning-tutorial

sourceHugging Faceupdated 1y agoView on Hugging Face
508likes
ResponsiveImage.astro470 linesDownload Raw Back to components
1---2// @ts-ignore - types provided by Astro at runtime3import { Image } from "astro:assets";4 5interface Props {6  /** Source image imported via astro:assets */7  src: any;8  /** Alt text for accessibility */9  alt: string;10  /** Optional HTML string caption (use slot caption for rich content) */11  caption?: string;12  /** Optional class to apply on the <figure> wrapper when caption is used */13  figureClass?: string;14  /** Enable medium-zoom behavior on this image */15  zoomable?: boolean;16  /** Show a download button overlay and enable download flow */17  downloadable?: boolean;18  /** Optional explicit file name to use on download */19  downloadName?: string;20  /** Optional explicit source URL to download instead of currentSrc */21  downloadSrc?: string;22  /** Optional link that wraps the image (not the caption) */23  linkHref?: string;24  /** Optional target for the link (default: _blank when linkHref provided) */25  linkTarget?: string;26  /** Optional rel for the link (default: noopener noreferrer when linkHref provided) */27  linkRel?: string;28  /** Any additional attributes should be forwarded to the underlying <Image> */29  [key: string]: any;30}31 32const {33  caption,34  figureClass,35  zoomable,36  downloadable,37  downloadName,38  downloadSrc,39  linkHref,40  linkTarget,41  linkRel,42  ...imgProps43} = Astro.props as Props;44const hasCaptionSlot = Astro.slots.has("caption");45const hasCaption =46  hasCaptionSlot || (typeof caption === "string" && caption.length > 0);47const uid = `ri_${Math.random().toString(36).slice(2)}`;48const dataZoomable =49  zoomable === true || (imgProps as any)["data-zoomable"] ? "1" : undefined;50const dataDownloadable =51  downloadable === true || (imgProps as any)["data-downloadable"]52    ? "1"53    : undefined;54const hasLink = typeof linkHref === "string" && linkHref.length > 0;55const resolvedTarget = hasLink ? linkTarget || "_blank" : undefined;56const resolvedRel = hasLink ? linkRel || "noopener noreferrer" : undefined;57---58 59<div class="ri-root" data-ri-root={uid}>60  {61    hasCaption ? (62      <figure63        class={(figureClass || "") + (dataDownloadable ? " has-dl-btn" : "")}64      >65        {dataDownloadable ? (66          <span class="img-dl-wrap">67            {hasLink ? (68              <a69                class="ri-link"70                href={linkHref}71                target={resolvedTarget}72                rel={resolvedRel}73              >74                <Image75                  {...imgProps}76                  data-zoomable={dataZoomable}77                  data-downloadable={dataDownloadable}78                  data-download-name={downloadName}79                  data-download-src={downloadSrc}80                />81              </a>82            ) : (83              <Image84                {...imgProps}85                data-zoomable={dataZoomable}86                data-downloadable={dataDownloadable}87                data-download-name={downloadName}88                data-download-src={downloadSrc}89              />90            )}91            <button92              type="button"93              class="button img-dl-btn"94              aria-label="Download image"95              title={96                downloadName ? `Download ${downloadName}` : "Download image"97              }98            >99              <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">100                <path d="M12 16c-.26 0-.52-.11-.71-.29l-5-5a1 1 0 0 1 1.42-1.42L11 12.59V4a1 1 0 1 1 2 0v8.59l3.29-3.3a1 1 0 1 1 1.42 1.42l-5 5c-.19.18-.45.29-.71.29zM5 20a1 1 0 1 1 0-2h14a1 1 0 1 1 0 2H5z" />101              </svg>102            </button>103          </span>104        ) : hasLink ? (105          <a106            class="ri-link"107            href={linkHref}108            target={resolvedTarget}109            rel={resolvedRel}110          >111            <Image {...imgProps} data-zoomable={dataZoomable} />112          </a>113        ) : (114          <Image {...imgProps} data-zoomable={dataZoomable} />115        )}116        <figcaption>117          {hasCaptionSlot ? (118            <slot name="caption" />119          ) : (120            caption && <span set:html={caption} />121          )}122        </figcaption>123      </figure>124    ) : dataDownloadable ? (125      <span class="img-dl-wrap">126        {hasLink ? (127          <a128            class="ri-link"129            href={linkHref}130            target={resolvedTarget}131            rel={resolvedRel}132          >133            <Image134              {...imgProps}135              data-zoomable={dataZoomable}136              data-downloadable={dataDownloadable}137              data-download-name={downloadName}138              data-download-src={downloadSrc}139            />140          </a>141        ) : (142          <Image143            {...imgProps}144            data-zoomable={dataZoomable}145            data-downloadable={dataDownloadable}146            data-download-name={downloadName}147            data-download-src={downloadSrc}148          />149        )}150        <button151          type="button"152          class="button img-dl-btn"153          aria-label="Download image"154          title={downloadName ? `Download ${downloadName}` : "Download image"}155        >156          <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">157            <path d="M12 16c-.26 0-.52-.11-.71-.29l-5-5a1 1 0 0 1 1.42-1.42L11 12.59V4a1 1 0 1 1 2 0v8.59l3.29-3.3a1 1 0 1 1 1.42 1.42l-5 5c-.19.18-.45.29-.71.29zM5 20a1 1 0 1 1 0-2h14a1 1 0 1 1 0 2H5z" />158          </svg>159        </button>160      </span>161    ) : hasLink ? (162      <a163        class="ri-link"164        href={linkHref}165        target={resolvedTarget}166        rel={resolvedRel}167      >168        <Image {...imgProps} data-zoomable={dataZoomable} />169      </a>170    ) : (171      <Image {...imgProps} data-zoomable={dataZoomable} />172    )173  }174</div>175 176<script is:inline>177  (() => {178    const scriptEl = document.currentScript;179    const root = scriptEl ? scriptEl.previousElementSibling : null;180    if (!root) return;181    const img =182      root.tagName === "IMG"183        ? root184        : root.querySelector185          ? root.querySelector("img")186          : null;187    if (!img) return;188 189    // medium-zoom integration scoped to this image only190    const ensureMediumZoomReady = (cb) => {191      // @ts-ignore192      if (window.mediumZoom) return cb();193      const retry = () => {194        // @ts-ignore195        if (window.mediumZoom) cb();196        else setTimeout(retry, 30);197      };198      retry();199    };200 201    const initZoomIfNeeded = () => {202      if (img.getAttribute("data-zoomable") !== "1") return;203      const isDark =204        document.documentElement.getAttribute("data-theme") === "dark";205      const background = isDark ? "rgba(0,0,0,.9)" : "rgba(0,0,0,.85)";206      ensureMediumZoomReady(() => {207        // @ts-ignore208        const instance = window.mediumZoom209          ? window.mediumZoom(img, { background, margin: 24, scrollOffset: 0 })210          : null;211        if (!instance) return;212        let onScrollLike;213        const attachCloseOnScroll = () => {214          if (onScrollLike) return;215          onScrollLike = () => {216            try {217              instance.close && instance.close();218            } catch {}219          };220          window.addEventListener("wheel", onScrollLike, { passive: true });221          window.addEventListener("touchmove", onScrollLike, { passive: true });222          window.addEventListener("scroll", onScrollLike, { passive: true });223        };224        const detachCloseOnScroll = () => {225          if (!onScrollLike) return;226          window.removeEventListener("wheel", onScrollLike);227          window.removeEventListener("touchmove", onScrollLike);228          window.removeEventListener("scroll", onScrollLike);229          onScrollLike = null;230        };231        try {232          instance.on && instance.on("open", attachCloseOnScroll);233        } catch {}234        try {235          instance.on && instance.on("close", detachCloseOnScroll);236        } catch {}237        const themeObserver = new MutationObserver(() => {238          const dark =239            document.documentElement.getAttribute("data-theme") === "dark";240          try {241            instance.update &&242              instance.update({243                background: dark ? "rgba(0,0,0,.9)" : "rgba(0,0,0,.85)",244              });245          } catch {}246        });247        themeObserver.observe(document.documentElement, {248          attributes: true,249          attributeFilter: ["data-theme"],250        });251      });252    };253 254    // Gestion zoom global pour masquer autres ResponsiveImage255    const setupGlobalZoomBehavior = () => {256      img.addEventListener("click", () => {257        if (img.getAttribute("data-zoomable") === "1") {258          // Enlever zoom-active de tous les autres ri-root259          document260            .querySelectorAll(".ri-root.zoom-active")261            .forEach((el) => el.classList.remove("zoom-active"));262 263          // Ajouter zoom-active à cet ri-root264          root.classList.add("zoom-active");265        }266      });267    };268 269    // Download button handler270    const dlBtn = root.querySelector ? root.querySelector(".img-dl-btn") : null;271    if (dlBtn) {272      dlBtn.addEventListener("click", async (ev) => {273        try {274          ev.preventDefault();275          ev.stopPropagation();276          const pickHrefAndName = () => {277            const current = img.currentSrc || img.src || "";278            let href = img.getAttribute("data-download-src") || current;279            const deriveName = () => {280              try {281                const u = new URL(current, location.href);282                const rawHref = u.searchParams.get("href");283                const candidate = rawHref284                  ? decodeURIComponent(rawHref)285                  : u.pathname;286                const last = String(candidate).split("/").pop() || "";287                const base = last.split("?")[0].split("#")[0];288                const m = base.match(289                  /^(.+?\.(?:png|jpe?g|webp|avif|gif|svg))(?:[._-].*)?$/i,290                );291                if (m && m[1]) return m[1];292                return base || "image";293              } catch {294                return "image";295              }296            };297            const name = img.getAttribute("data-download-name") || deriveName();298            return { href, name };299          };300          const picked = pickHrefAndName();301          const res = await fetch(picked.href, { credentials: "same-origin" });302          const blob = await res.blob();303          const objectUrl = URL.createObjectURL(blob);304          const tmp = document.createElement("a");305          tmp.href = objectUrl;306          tmp.download = picked.name || "image";307          tmp.target = "_self";308          tmp.rel = "noopener";309          tmp.style.display = "none";310          document.body.appendChild(tmp);311          tmp.click();312          setTimeout(() => {313            URL.revokeObjectURL(objectUrl);314            tmp.remove();315          }, 1000);316        } catch {}317      });318    }319 320    // Setup comportement zoom321    setupGlobalZoomBehavior();322 323    if (document.readyState === "complete") initZoomIfNeeded();324    else window.addEventListener("load", initZoomIfNeeded, { once: true });325  })();326</script>327 328<style>329  figure {330    margin: var(--block-spacing-y) 0;331  }332  figcaption {333    text-align: left;334    font-size: 0.9rem;335    color: var(--muted-color);336    margin-top: 6px;337  }338  figcaption {339    background: var(--page-bg);340    position: relative;341    z-index: var(--z-elevated);342    display: block;343    width: 100%;344  }345  .image-credit {346    display: block;347    margin-top: 4px;348    font-size: 12px;349    color: var(--muted-color);350  }351  .image-credit a {352    color: inherit;353    text-decoration: underline;354    text-underline-offset: 2px;355  }356 357  /* Zoomable overlay container (if used by any lightbox implementation) */358  [data-zoom-overlay],359  .zoom-overlay {360    position: fixed;361    inset: 0;362    z-index: var(--z-overlay);363  }364 365  /* Download link inside figures */366  figure .download-link {367    position: relative;368    z-index: var(--z-elevated);369  }370 371  /* Opt-in zoomable images */372  img[data-zoomable] {373    cursor: zoom-in;374  }375  .medium-zoom--opened img[data-zoomable] {376    cursor: zoom-out;377  }378 379  /* Download button for img[data-downloadable] */380  figure.has-dl-btn {381    position: relative;382  }383  .dl-host {384    position: relative;385  }386  .img-dl-wrap {387    position: relative;388    display: inline-block;389  }390  .img-dl-btn {391    position: absolute;392    right: 8px;393    bottom: 8px;394    align-items: center;395    justify-content: center;396    width: 30px;397    height: 30px;398    border-radius: 6px;399    color: white;400    text-decoration: none;401    border: 1px solid rgba(255, 255, 255, 0.25);402    z-index: var(--z-elevated);403    display: none;404    background: var(--primary-color);405  }406 407  /* Quand une image est zoomée, cacher TOUS les ResponsiveImage de la page */408  :global(.medium-zoom--opened) .ri-root {409    opacity: 0;410    z-index: calc(var(--z-base) - 1);411    transition: opacity 0.3s ease;412  }413 414  /* L'image actuellement zoomée reste visible */415  :global(.medium-zoom--opened) .ri-root:has(.medium-zoom--opened) {416    opacity: 1;417    z-index: var(--z-overlay);418  }419 420  /* Fallback pour navigateurs sans support :has() */421  :global(.medium-zoom--opened) .ri-root.zoom-active {422    opacity: 1 !important;423    z-index: var(--z-overlay) !important;424  }425 426  /* Spécifiquement masquer bouton download et figcaption lors du zoom */427  :global(.medium-zoom--opened) .img-dl-btn {428    opacity: 0;429    z-index: calc(var(--z-base) - 1);430    transition: opacity 0.3s ease;431  }432 433  :global(.medium-zoom--opened) figcaption {434    opacity: 0;435    z-index: calc(var(--z-base) - 1);436    transition: opacity 0.3s ease;437  }438 439  /* Même pour l'image zoomée active, masquer bouton et caption pour une expérience propre */440  :global(.medium-zoom--opened) .ri-root.zoom-active .img-dl-btn {441    opacity: 0;442    z-index: calc(var(--z-base) - 1);443  }444 445  :global(.medium-zoom--opened) .ri-root.zoom-active figcaption {446    opacity: 0;447    z-index: calc(var(--z-base) - 1);448  }449  .img-dl-btn svg {450    width: 18px;451    height: 18px;452    fill: currentColor;453  }454  .img-dl-wrap:hover .img-dl-btn {455    display: inline-flex;456  }457  .img-dl-btn:hover {458    background: var(--primary-color-hover);459  }460 461  [data-theme="dark"] .img-dl-btn {462    background: var(--primary-color);463    color: var(--on-primary);464    border-color: var(--primary-color);465  }466  [data-theme="dark"] .img-dl-btn:hover {467    background: var(--primary-color-hover);468  }469</style>470