lerobot/robot-learning-tutorial
508
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 