const themeButtons = document.querySelectorAll("[data-theme-toggle]"); const themeStorageKey = "pi-web-theme"; const systemPrefersLight = window.matchMedia("(prefers-color-scheme: light)"); function storedThemeMode() { const theme = window.localStorage.getItem(themeStorageKey); return theme === "light" || theme === "dark" || theme === "auto" ? theme : "auto"; } function activeTheme() { return document.documentElement.dataset.theme === "light" || document.documentElement.dataset.theme === "dark" ? document.documentElement.dataset.theme : systemPrefersLight.matches ? "light" : "dark"; } function applyThemeMode(mode) { if (mode === "light" || mode === "dark") { document.documentElement.dataset.theme = mode; } else { delete document.documentElement.dataset.theme; } window.localStorage.setItem(themeStorageKey, mode); updateThemeButtons(mode); } function updateThemeButtons(mode = storedThemeMode()) { const active = activeTheme(); for (const button of themeButtons) { button.dataset.theme = mode; button.dataset.activeTheme = active; const icon = button.querySelector("[data-theme-icon]"); const label = button.querySelector("[data-theme-label]"); if (mode === "auto") { button.setAttribute("aria-label", `Theme: Auto (${active}). Click to use dark theme.`); if (icon !== null) icon.textContent = "◐"; if (label !== null) label.textContent = "Auto"; } else if (mode === "light") { button.setAttribute("aria-label", "Theme: Light. Click to use automatic theme."); if (icon !== null) icon.textContent = "☀"; if (label !== null) label.textContent = "Light"; } else { button.setAttribute("aria-label", "Theme: Dark. Click to use light theme."); if (icon !== null) icon.textContent = "☾"; if (label !== null) label.textContent = "Dark"; } } } updateThemeButtons(); systemPrefersLight.addEventListener("change", () => { if (storedThemeMode() === "auto") updateThemeButtons("auto"); }); for (const button of themeButtons) { button.addEventListener("click", () => { const currentMode = storedThemeMode(); const nextTheme = currentMode === "auto" ? "dark" : currentMode === "dark" ? "light" : "auto"; applyThemeMode(nextTheme); }); } const screenshotCarousels = document.querySelectorAll("[data-demo-carousel]"); const reducedMotionQuery = window.matchMedia("(prefers-reduced-motion: reduce)"); function setupScreenshotCarousel(carousel) { const gallery = carousel.querySelector("[data-demo-gallery]"); const controls = carousel.querySelector("[data-demo-controls]"); const previousButton = carousel.querySelector("[data-demo-prev]"); const nextButton = carousel.querySelector("[data-demo-next]"); const dots = Array.from(carousel.querySelectorAll("[data-demo-dot]")); const slides = Array.from(carousel.querySelectorAll("[data-demo-slide]")); const lightbox = carousel.querySelector("[data-demo-lightbox]"); const lightboxImage = carousel.querySelector("[data-demo-lightbox-image]"); const lightboxCaption = carousel.querySelector("[data-demo-lightbox-caption]"); const lightboxCloseButton = carousel.querySelector("[data-demo-lightbox-close]"); const lightboxTriggers = Array.from(carousel.querySelectorAll("[data-demo-lightbox-trigger]")); if (gallery === null || slides.length === 0) return; let updateQueued = false; function galleryHasOverflow() { return gallery.scrollWidth > gallery.clientWidth + 4; } function closestSlideIndex() { const galleryRect = gallery.getBoundingClientRect(); const galleryCenter = galleryRect.left + galleryRect.width / 2; let closestIndex = 0; let closestDistance = Number.POSITIVE_INFINITY; slides.forEach((slide, index) => { const rect = slide.getBoundingClientRect(); const distance = Math.abs(rect.left + rect.width / 2 - galleryCenter); if (distance < closestDistance) { closestIndex = index; closestDistance = distance; } }); return closestIndex; } function scrollToSlide(index) { const slide = slides[index]; if (slide === undefined) return; slide.scrollIntoView({ behavior: reducedMotionQuery.matches ? "auto" : "smooth", block: "nearest", inline: "start", }); } function closeLightbox() { if (lightbox === null) return; if (typeof lightbox.close === "function" && lightbox.open) { lightbox.close(); } else { lightbox.removeAttribute("open"); } } function openLightbox(trigger) { const image = trigger.querySelector("img"); if (image === null || lightbox === null || lightboxImage === null) return; const figure = trigger.closest("figure"); const captionParts = Array.from(figure?.querySelectorAll("figcaption strong, figcaption span") ?? []) .map((node) => node.textContent?.trim()) .filter(Boolean); const caption = captionParts.length > 0 ? captionParts.join(" — ") : "PI WEB screenshot"; lightboxImage.src = image.currentSrc || image.src; lightboxImage.alt = image.alt; if (lightboxCaption !== null) lightboxCaption.textContent = caption; if (typeof lightbox.showModal === "function") { lightbox.showModal(); } else { lightbox.setAttribute("open", ""); } lightboxCloseButton?.focus({ preventScroll: true }); } function updateControls() { const overflow = galleryHasOverflow(); const activeIndex = closestSlideIndex(); const atStart = gallery.scrollLeft <= 2; const atEnd = gallery.scrollLeft + gallery.clientWidth >= gallery.scrollWidth - 2; carousel.dataset.overflow = overflow ? "true" : "false"; gallery.tabIndex = overflow ? 0 : -1; if (controls !== null) controls.hidden = !overflow; if (previousButton !== null) previousButton.disabled = !overflow || atStart; if (nextButton !== null) nextButton.disabled = !overflow || atEnd; dots.forEach((dot, index) => { dot.setAttribute("aria-current", index === activeIndex ? "true" : "false"); }); } function queueUpdateControls() { if (updateQueued) return; updateQueued = true; window.requestAnimationFrame(() => { updateQueued = false; updateControls(); }); } previousButton?.addEventListener("click", () => { scrollToSlide(Math.max(closestSlideIndex() - 1, 0)); }); nextButton?.addEventListener("click", () => { scrollToSlide(Math.min(closestSlideIndex() + 1, slides.length - 1)); }); dots.forEach((dot) => { const targetIndex = Number.parseInt(dot.getAttribute("data-demo-dot") ?? "", 10); if (Number.isNaN(targetIndex)) return; dot.addEventListener("click", () => { scrollToSlide(targetIndex); }); }); lightboxTriggers.forEach((trigger) => { trigger.addEventListener("click", () => { openLightbox(trigger); }); }); lightboxCloseButton?.addEventListener("click", closeLightbox); lightbox?.addEventListener("click", (event) => { if (event.target === lightbox) closeLightbox(); }); lightbox?.addEventListener("close", () => { lightboxImage?.removeAttribute("src"); }); gallery.addEventListener("scroll", queueUpdateControls, { passive: true }); window.addEventListener("resize", queueUpdateControls); if ("ResizeObserver" in window) { const resizeObserver = new window.ResizeObserver(queueUpdateControls); resizeObserver.observe(gallery); slides.forEach((slide) => resizeObserver.observe(slide)); } updateControls(); } for (const carousel of screenshotCarousels) { setupScreenshotCarousel(carousel); } const copyButtons = document.querySelectorAll("[data-copy]"); for (const button of copyButtons) { button.addEventListener("click", async () => { const targetSelector = button.getAttribute("data-copy"); const target = targetSelector === null ? null : document.querySelector(targetSelector); const text = target?.textContent?.replace(/^\s*\$ /gm, "").trim(); if (!text) return; try { await navigator.clipboard.writeText(text); const original = button.textContent; button.textContent = "Copied"; window.setTimeout(() => { button.textContent = original; }, 1400); } catch { button.textContent = "Select code"; } }); }