lerobot/robot-learning-tutorial
508
1---2// @ts-ignore - types provided by Astro at runtime3import ResponsiveImage from "./ResponsiveImage.astro";4 5interface ImageItem {6 /** Source image imported via astro:assets */7 src: any;8 /** Alt text for accessibility */9 alt: string;10 /** Individual caption for this image */11 caption?: string;12 /** Optional individual image ID for referencing */13 id?: string;14 /** Enable zoom on this specific image (defaults to parent zoomable setting) */15 zoomable?: boolean;16 /** Enable download on this specific image (defaults to parent downloadable setting) */17 downloadable?: boolean;18}19 20interface Props {21 /** Array of images to display */22 images: ImageItem[];23 /** Global caption for the entire figure */24 caption?: string;25 /** Layout mode: number of columns or 'auto' for responsive */26 layout?: "2-column" | "3-column" | "4-column" | "auto";27 /** Enable medium-zoom behavior on all images (can be overridden per image) */28 zoomable?: boolean;29 /** Show download buttons on all images (can be overridden per image) */30 downloadable?: boolean;31 /** Optional class to apply on the wrapper */32 class?: string;33 /** Optional global ID for the multi-image figure */34 id?: string;35}36 37const {38 images,39 caption,40 layout = "3-column",41 zoomable = false,42 downloadable = false,43 class: className,44 id,45} = Astro.props as Props;46 47const hasCaptionSlot = Astro.slots.has("caption");48const hasCaption =49 hasCaptionSlot || (typeof caption === "string" && caption.length > 0);50const uid = `mi_${Math.random().toString(36).slice(2)}`;51 52// Generate CSS grid columns based on layout53const getGridColumns = () => {54 switch (layout) {55 case "2-column":56 return "repeat(2, 1fr)";57 case "3-column":58 return "repeat(3, 1fr)";59 case "4-column":60 return "repeat(4, 1fr)";61 case "auto":62 return "repeat(auto-fit, minmax(200px, 1fr))";63 default:64 return "repeat(3, 1fr)";65 }66};67 68const gridColumns = getGridColumns();69---70 71<div72 class={`multi-image ${className || ""}`}73 data-mi-root={uid}74 data-layout={layout}75 {id}76>77 {78 hasCaption ? (79 <figure class="multi-image-figure">80 <div81 class="multi-image-grid"82 style={`grid-template-columns: ${gridColumns}`}83 >84 {images.map((image, index) => (85 <div class="multi-image-item">86 <ResponsiveImage87 src={image.src}88 alt={image.alt}89 zoomable={image.zoomable ?? zoomable}90 downloadable={91 image.downloadable ?? downloadable92 }93 class="multi-image-img"94 />95 {image.caption && (96 <div class="multi-image-subcaption">97 {image.caption}98 </div>99 )}100 {image.id && (101 <span102 id={image.id}103 style="position: absolute;"104 />105 )}106 </div>107 ))}108 </div>109 <figcaption class="multi-image-caption">110 {hasCaptionSlot ? (111 <slot name="caption" />112 ) : (113 caption && <span set:html={caption} />114 )}115 </figcaption>116 </figure>117 ) : (118 <div119 class="multi-image-grid"120 style={`grid-template-columns: ${gridColumns}`}121 >122 {images.map((image, index) => (123 <div class="multi-image-item">124 <ResponsiveImage125 src={image.src}126 alt={image.alt}127 zoomable={image.zoomable ?? zoomable}128 downloadable={image.downloadable ?? downloadable}129 class="multi-image-img"130 />131 {image.caption && (132 <div class="multi-image-subcaption">133 {image.caption}134 </div>135 )}136 {image.id && (137 <span id={image.id} style="position: absolute;" />138 )}139 </div>140 ))}141 </div>142 )143 }144</div>145 146<style>147 .multi-image {148 margin: var(--block-spacing-y) 0;149 }150 151 .multi-image-figure {152 margin: 0;153 }154 155 .multi-image-grid {156 display: grid;157 gap: 1rem;158 align-items: start;159 }160 161 .multi-image-item {162 display: flex;163 flex-direction: column;164 text-align: center;165 position: relative;166 z-index: var(--z-content);167 transition: z-index 0.3s ease;168 }169 170 /* Quand medium-zoom est actif, masquer temporairement les autres images du multi-image */171 :global(.medium-zoom--opened) .multi-image-item {172 opacity: 0;173 z-index: calc(var(--z-base) - 1);174 transition:175 opacity 0.3s ease,176 z-index 0.3s ease;177 }178 179 /* L'image actuellement zoomée reste visible */180 :global(.medium-zoom--opened)181 .multi-image-item:has(:global(.medium-zoom--opened)) {182 opacity: 1;183 z-index: var(--z-overlay);184 }185 186 /* Fallback pour navigateurs sans support :has() */187 :global(.medium-zoom--opened) .multi-image-item.zoom-active {188 opacity: 1 !important;189 z-index: var(--z-overlay) !important;190 }191 192 .multi-image-item :global(.ri-root) {193 margin: 0;194 }195 196 .multi-image-item :global(figure) {197 margin: 0;198 }199 200 .multi-image-img {201 width: 100%;202 height: auto;203 object-fit: contain;204 }205 206 .multi-image-subcaption {207 font-size: 0.85rem;208 color: var(--muted-color);209 margin-top: 0.5rem;210 line-height: 1.4;211 }212 213 .multi-image-caption {214 text-align: left;215 font-size: 0.9rem;216 color: var(--muted-color);217 margin-top: 1rem;218 line-height: 1.4;219 }220 221 /* Responsive behavior */222 @media (max-width: 768px) {223 .multi-image-grid[style*="repeat(3, 1fr)"],224 .multi-image-grid[style*="repeat(4, 1fr)"] {225 grid-template-columns: 1fr !important;226 gap: 1.5rem;227 }228 229 .multi-image-grid[style*="repeat(2, 1fr)"] {230 grid-template-columns: 1fr !important;231 gap: 1.5rem;232 }233 }234 235 @media (min-width: 769px) and (max-width: 1024px) {236 .multi-image-grid[style*="repeat(4, 1fr)"] {237 grid-template-columns: repeat(2, 1fr) !important;238 }239 }240 241 /* Equal height images when desired */242 .multi-image[data-layout*="column"] .multi-image-item :global(img) {243 height: 200px;244 object-fit: contain;245 }246 247 /* Auto layout gets flexible heights */248 .multi-image[data-layout="auto"] .multi-image-item :global(img) {249 height: auto;250 }251 252 /* Ensure images maintain aspect ratio */253 .multi-image-item :global(img) {254 max-width: 100%;255 display: block;256 margin: 0 auto;257 }258</style>259 260<script>261 // Enhanced medium-zoom integration for MultiImage262 document.addEventListener("DOMContentLoaded", () => {263 // Amélioration du comportement des MultiImage avec medium-zoom264 const multiImages = document.querySelectorAll(".multi-image");265 266 multiImages.forEach((multiImage) => {267 const items = multiImage.querySelectorAll(".multi-image-item");268 const zoomableImages = multiImage.querySelectorAll(269 'img[data-zoomable="1"]',270 );271 272 zoomableImages.forEach((img) => {273 img.addEventListener("click", () => {274 // Trouver l'item parent de l'image cliquée et le ri-root275 const activeItem = img.closest(".multi-image-item");276 const riRoot = img.closest(".ri-root");277 278 // Nettoyer TOUS les zoom-active (MultiImage items et ResponsiveImage)279 document280 .querySelectorAll(281 ".multi-image-item.zoom-active, .ri-root.zoom-active",282 )283 .forEach((el) => el.classList.remove("zoom-active"));284 285 // Ajouter zoom-active aux éléments actifs286 if (activeItem) {287 activeItem.classList.add("zoom-active");288 }289 if (riRoot) {290 riRoot.classList.add("zoom-active");291 }292 });293 });294 });295 296 // Nettoyer TOUTES les classes lors de la fermeture du zoom297 document.addEventListener("click", (e) => {298 if (e.target.classList.contains("medium-zoom-overlay")) {299 // Zoom fermé, nettoyer toutes les classes zoom-active300 document301 .querySelectorAll(302 ".multi-image-item.zoom-active, .ri-root.zoom-active",303 )304 .forEach((item) => item.classList.remove("zoom-active"));305 }306 });307 308 // Écouter les événements clavier pour fermer le zoom309 document.addEventListener("keydown", (e) => {310 if (e.key === "Escape") {311 document312 .querySelectorAll(313 ".multi-image-item.zoom-active, .ri-root.zoom-active",314 )315 .forEach((item) => item.classList.remove("zoom-active"));316 }317 });318 });319</script>320 