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