CoolFace
Apppublic

lerobot/robot-learning-tutorial

sourceHugging Faceupdated 1y agoView on Hugging Face
508likes
Image.astro509 linesDownload Raw Back to components
1---2// @ts-ignore - types provided by Astro at runtime3import { Image as AstroImage } 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  /** Make the image span full width */29  fullWidth?: boolean;30  /** Any additional attributes should be forwarded to the underlying <AstroImage> */31  [key: string]: any;32}33 34const {35  caption,36  figureClass,37  zoomable,38  downloadable,39  downloadName,40  downloadSrc,41  linkHref,42  linkTarget,43  linkRel,44  fullWidth,45  ...imgProps46} = Astro.props as Props;47const hasCaptionSlot = Astro.slots.has("caption");48const hasCaption =49  hasCaptionSlot || (typeof caption === "string" && caption.length > 0);50const hasTitle = Astro.slots.has("title");51const uid = `ri_${Math.random().toString(36).slice(2)}`;52const dataZoomable =53  zoomable === true || (imgProps as any)["data-zoomable"] ? "1" : undefined;54const dataDownloadable =55  downloadable === true || (imgProps as any)["data-downloadable"]56    ? "1"57    : undefined;58const hasLink = typeof linkHref === "string" && linkHref.length > 0;59const resolvedTarget = hasLink ? linkTarget || "_blank" : undefined;60const resolvedRel = hasLink ? linkRel || "noopener noreferrer" : undefined;61---62 63<div64  class={`ri-root`}65  data-ri-root={uid}66  data-has-title={hasTitle}67  data-has-caption={hasCaption}68>69  {70    hasCaption ? (71      <figure72        class={(figureClass || "") + (dataDownloadable ? " has-dl-btn" : "")}73      >74        {dataDownloadable ? (75          <span class="img-dl-wrap">76            {hasLink ? (77              <a78                class="ri-link"79                href={linkHref}80                target={resolvedTarget}81                rel={resolvedRel}82              >83                <AstroImage84                  {...imgProps}85                  data-zoomable={dataZoomable}86                  data-downloadable={dataDownloadable}87                  data-download-name={downloadName}88                  data-download-src={downloadSrc}89                />90              </a>91            ) : (92              <AstroImage93                {...imgProps}94                data-zoomable={dataZoomable}95                data-downloadable={dataDownloadable}96                data-download-name={downloadName}97                data-download-src={downloadSrc}98              />99            )}100            <button101              type="button"102              class="button img-dl-btn"103              aria-label="Download image"104              title={105                downloadName ? `Download ${downloadName}` : "Download image"106              }107            >108              <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">109                <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" />110              </svg>111            </button>112          </span>113        ) : hasLink ? (114          <a115            class="ri-link"116            href={linkHref}117            target={resolvedTarget}118            rel={resolvedRel}119          >120            <AstroImage {...imgProps} data-zoomable={dataZoomable} />121          </a>122        ) : (123          <AstroImage {...imgProps} data-zoomable={dataZoomable} />124        )}125        <figcaption>126          {hasCaptionSlot ? (127            <slot name="caption" />128          ) : (129            caption && <span set:html={caption} />130          )}131        </figcaption>132      </figure>133    ) : dataDownloadable ? (134      <span class="img-dl-wrap">135        {hasLink ? (136          <a137            class="ri-link"138            href={linkHref}139            target={resolvedTarget}140            rel={resolvedRel}141          >142            <AstroImage143              {...imgProps}144              data-zoomable={dataZoomable}145              data-downloadable={dataDownloadable}146              data-download-name={downloadName}147              data-download-src={downloadSrc}148            />149          </a>150        ) : (151          <AstroImage152            {...imgProps}153            data-zoomable={dataZoomable}154            data-downloadable={dataDownloadable}155            data-download-name={downloadName}156            data-download-src={downloadSrc}157          />158        )}159        <button160          type="button"161          class="button img-dl-btn"162          aria-label="Download image"163          title={downloadName ? `Download ${downloadName}` : "Download image"}164        >165          <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">166            <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" />167          </svg>168        </button>169      </span>170    ) : hasLink ? (171      <a172        class="ri-link"173        href={linkHref}174        target={resolvedTarget}175        rel={resolvedRel}176      >177        <AstroImage178          {...imgProps}179          data-zoomable={dataZoomable}180          class={fullWidth ? "full" : ""}181        />182      </a>183    ) : (184      <AstroImage185        {...imgProps}186        data-zoomable={dataZoomable}187        class={fullWidth ? "full" : ""}188      />189    )190  }191</div>192 193<script is:inline>194  (() => {195    const scriptEl = document.currentScript;196    const root = scriptEl ? scriptEl.previousElementSibling : null;197    if (!root) {198      console.log("Figure script: No root element found, exiting");199      return;200    }201    const img =202      root.tagName === "IMG"203        ? root204        : root.querySelector205          ? root.querySelector("img")206          : null;207    if (!img) {208      console.log("Figure script: No img element found, exiting");209      return;210    }211 212    // medium-zoom integration scoped to this image only213    const ensureMediumZoomReady = (cb) => {214      // @ts-ignore215      if (window.mediumZoom) return cb();216      const retry = () => {217        // @ts-ignore218        if (window.mediumZoom) cb();219        else setTimeout(retry, 30);220      };221      retry();222    };223 224    const initZoomIfNeeded = () => {225      if (img.getAttribute("data-zoomable") !== "1") return;226      const isDark =227        document.documentElement.getAttribute("data-theme") === "dark";228      const background = isDark ? "rgba(0,0,0,.9)" : "rgba(0,0,0,.85)";229      ensureMediumZoomReady(() => {230        // @ts-ignore231        const instance = window.mediumZoom232          ? window.mediumZoom(img, { background, margin: 24, scrollOffset: 0 })233          : null;234        if (!instance) return;235        let onScrollLike;236        const attachCloseOnScroll = () => {237          if (onScrollLike) return;238          onScrollLike = () => {239            try {240              instance.close && instance.close();241            } catch {}242          };243          window.addEventListener("wheel", onScrollLike, { passive: true });244          window.addEventListener("touchmove", onScrollLike, { passive: true });245          window.addEventListener("scroll", onScrollLike, { passive: true });246        };247        const detachCloseOnScroll = () => {248          if (!onScrollLike) return;249          window.removeEventListener("wheel", onScrollLike);250          window.removeEventListener("touchmove", onScrollLike);251          window.removeEventListener("scroll", onScrollLike);252          onScrollLike = null;253        };254        try {255          instance.on && instance.on("open", attachCloseOnScroll);256        } catch {}257        try {258          instance.on && instance.on("close", detachCloseOnScroll);259        } catch {}260        const themeObserver = new MutationObserver(() => {261          const dark =262            document.documentElement.getAttribute("data-theme") === "dark";263          try {264            instance.update &&265              instance.update({266                background: dark ? "rgba(0,0,0,.9)" : "rgba(0,0,0,.85)",267              });268          } catch {}269        });270        themeObserver.observe(document.documentElement, {271          attributes: true,272          attributeFilter: ["data-theme"],273        });274      });275    };276 277    // Global zoom management to hide other Figures278    const setupGlobalZoomBehavior = () => {279      img.addEventListener("click", () => {280        if (img.getAttribute("data-zoomable") === "1") {281          // Enlever zoom-active de tous les autres ri-root282          document283            .querySelectorAll(".ri-root.zoom-active")284            .forEach((el) => el.classList.remove("zoom-active"));285 286          // Add zoom-active to this ri-root287          root.classList.add("zoom-active");288        }289      });290    };291 292    // Download button handler293    const dlBtn = root.querySelector ? root.querySelector(".img-dl-btn") : null;294    if (dlBtn) {295      dlBtn.addEventListener("click", async (ev) => {296        try {297          ev.preventDefault();298          ev.stopPropagation();299          const pickHrefAndName = () => {300            const current = img.currentSrc || img.src || "";301            let href = img.getAttribute("data-download-src") || current;302            const deriveName = () => {303              try {304                const u = new URL(current, location.href);305                const rawHref = u.searchParams.get("href");306                const candidate = rawHref307                  ? decodeURIComponent(rawHref)308                  : u.pathname;309                const last = String(candidate).split("/").pop() || "";310                const base = last.split("?")[0].split("#")[0];311                const m = base.match(312                  /^(.+?\.(?:png|jpe?g|webp|avif|gif|svg))(?:[._-].*)?$/i,313                );314                if (m && m[1]) return m[1];315                return base || "image";316              } catch {317                return "image";318              }319            };320            const name = img.getAttribute("data-download-name") || deriveName();321            return { href, name };322          };323          const picked = pickHrefAndName();324          const res = await fetch(picked.href, { credentials: "same-origin" });325          const blob = await res.blob();326          const objectUrl = URL.createObjectURL(blob);327          const tmp = document.createElement("a");328          tmp.href = objectUrl;329          tmp.download = picked.name || "image";330          tmp.target = "_self";331          tmp.rel = "noopener";332          tmp.style.display = "none";333          document.body.appendChild(tmp);334          tmp.click();335          setTimeout(() => {336            URL.revokeObjectURL(objectUrl);337            tmp.remove();338          }, 1000);339        } catch {}340      });341    }342 343    // Setup comportement zoom344    setupGlobalZoomBehavior();345 346    if (document.readyState === "complete") initZoomIfNeeded();347    else window.addEventListener("load", initZoomIfNeeded, { once: true });348  })();349</script>350 351<style>352  figure {353    margin: var(--block-spacing-y) 0;354  }355  figcaption {356    text-align: left;357    font-size: 0.9rem;358    color: var(--muted-color);359    margin-top: 6px;360  }361  figcaption {362    background: var(--page-bg);363    position: relative;364    z-index: var(--z-elevated);365    display: block;366    width: 100%;367  }368  .image-credit {369    display: block;370    margin-top: 4px;371    font-size: 12px;372    color: var(--muted-color);373  }374  .image-credit a {375    color: inherit;376    text-decoration: underline;377    text-underline-offset: 2px;378  }379 380  /* Zoomable overlay container (if used by any lightbox implementation) */381  [data-zoom-overlay],382  .zoom-overlay {383    position: fixed;384    inset: 0;385    z-index: var(--z-overlay);386  }387 388  /* Download link inside figures */389  figure .download-link {390    position: relative;391    z-index: var(--z-elevated);392  }393 394  /* Opt-in zoomable images */395  img[data-zoomable] {396    cursor: zoom-in;397  }398  .medium-zoom--opened img[data-zoomable] {399    cursor: zoom-out;400  }401 402  /* Download button for img[data-downloadable] */403  figure.has-dl-btn {404    position: relative;405  }406  .dl-host {407    position: relative;408  }409  .img-dl-wrap {410    position: relative;411    display: inline-block;412  }413  .img-dl-btn {414    position: absolute;415    right: 8px;416    bottom: 8px;417    align-items: center;418    justify-content: center;419    width: 30px;420    height: 30px;421    border-radius: 6px;422    color: white;423    text-decoration: none;424    border: 1px solid rgba(255, 255, 255, 0.25);425    z-index: var(--z-elevated);426    display: none;427    background: var(--primary-color);428  }429 430  /* When an image is zoomed, hide ALL Figures on the page */431  :global(.medium-zoom--opened) .ri-root {432    opacity: 0;433    z-index: calc(var(--z-base) - 1);434    transition: opacity 0.3s ease;435  }436 437  /* The currently zoomed image remains visible */438  :global(.medium-zoom--opened) .ri-root:has(.medium-zoom--opened) {439    opacity: 1;440    z-index: var(--z-overlay);441  }442 443  /* Fallback for browsers without :has() support */444  :global(.medium-zoom--opened) .ri-root.zoom-active {445    opacity: 1 !important;446    z-index: var(--z-overlay) !important;447  }448 449  /* Specifically hide download button and figcaption during zoom */450  :global(.medium-zoom--opened) .img-dl-btn {451    opacity: 0;452    z-index: calc(var(--z-base) - 1);453    transition: opacity 0.3s ease;454  }455 456  :global(.medium-zoom--opened) figcaption {457    opacity: 0;458    z-index: calc(var(--z-base) - 1);459    transition: opacity 0.3s ease;460  }461 462  /* Even for active zoomed image, hide button and caption for clean experience */463  :global(.medium-zoom--opened) .ri-root.zoom-active .img-dl-btn {464    opacity: 0;465    z-index: calc(var(--z-base) - 1);466  }467 468  :global(.medium-zoom--opened) .ri-root.zoom-active figcaption {469    opacity: 0;470    z-index: calc(var(--z-base) - 1);471  }472  .img-dl-btn svg {473    width: 18px;474    height: 18px;475    fill: currentColor;476  }477  .img-dl-wrap:hover .img-dl-btn {478    display: inline-flex;479  }480  .img-dl-btn:hover {481    background: var(--primary-color-hover);482  }483 484  [data-theme="dark"] .img-dl-btn {485    background: var(--primary-color);486    color: var(--on-primary);487    border-color: var(--primary-color);488  }489  [data-theme="dark"] .img-dl-btn:hover {490    background: var(--primary-color-hover);491  }492 493  /* Conditional margins based on title and caption presence */494  .ri-root:not([data-has-title="true"]) {495    margin-top: 20px;496  }497 498  .ri-root:not([data-has-caption="true"]) {499    margin-bottom: 20px;500  }501 502  /* full image styles */503  img.full {504    width: 100% !important;505    min-width: 100%;506    max-width: 100%;507  }508</style>509