export interface LightboxImage { src: string; alt: string; width?: number; height?: number; origin?: HTMLElement; } class LightboxState { images = $state([]); index = $state(0); open = $state(false); readonly current = $derived(this.images[this.index]); show(images: LightboxImage[], index: number): void { if (images.length === 0) return; this.images = images; this.index = Math.min(Math.max(index, 0), images.length - 1); this.open = true; } close(): void { this.open = false; } step(delta: number): void { const count = this.images.length; if (count < 2) return; this.index = (this.index + delta + count) % count; } } export const lightbox = new LightboxState(); const zoomable = (image: HTMLImageElement): boolean => image.closest("a") === null; const describe = (image: HTMLImageElement): LightboxImage => ({ src: image.currentSrc || image.src, alt: image.alt, width: image.naturalWidth || undefined, height: image.naturalHeight || undefined, origin: image }); export const zoomMarkupImages = (): (() => void) => { const onClick = (event: MouseEvent): void => { if (event.defaultPrevented || event.button !== 0) return; if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; const image = event.target; if (!(image instanceof HTMLImageElement) || !zoomable(image)) return; const markup = image.closest(".markup"); if (!markup) return; const gallery = [...markup.querySelectorAll("img")].filter(zoomable); const index = gallery.indexOf(image); if (index === -1) return; event.preventDefault(); lightbox.show(gallery.map(describe), index); }; document.addEventListener("click", onClick); return () => document.removeEventListener("click", onClick); };