From 790b36f0eae65f7ebb8daab1676c1adc6f1c35f3 Mon Sep 17 00:00:00 2001 From: Federico Jaramillo Martinez Date: Thu, 25 Jun 2026 15:50:45 +0200 Subject: [PATCH] docs: improve screenshot gallery --- docs/index.html | 59 +++++++--- docs/site.js | 161 +++++++++++++++++++++++++++ docs/styles.css | 283 ++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 469 insertions(+), 34 deletions(-) diff --git a/docs/index.html b/docs/index.html index bfa5fce..e17f81b 100644 --- a/docs/index.html +++ b/docs/index.html @@ -147,25 +147,58 @@
-
+
- Workspaces, sessions, transcripts, files — one Pi web UI on every screen. - Bring your own repositories. +
+ Workspaces, sessions, transcripts, files — one Pi web UI on every screen. + Bring your own repositories. Swipe through the screenshots or click one to enlarge. +
+
-
diff --git a/docs/site.js b/docs/site.js index 95672c7..23b9bec 100644 --- a/docs/site.js +++ b/docs/site.js @@ -61,6 +61,167 @@ for (const button of themeButtons) { }); } +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) { diff --git a/docs/styles.css b/docs/styles.css index 9ea31df..f983592 100644 --- a/docs/styles.css +++ b/docs/styles.css @@ -610,52 +610,255 @@ code .comment, .demo-caption { display: flex; align-items: center; + flex-wrap: wrap; justify-content: space-between; - gap: 16px; + gap: 14px 18px; padding: 15px 18px; border-bottom: 1px solid var(--line); background: #0c1020; color: var(--muted); } -.demo-gallery { +.demo-caption-copy { display: grid; - grid-template-columns: minmax(0, 1.25fr) minmax(220px, 0.72fr); + min-width: min(100%, 340px); + gap: 3px; +} + +.demo-caption-copy span { + color: var(--muted-2); +} + +.demo-controls { + display: flex; + align-items: center; + gap: 10px; + margin-left: auto; +} + +.demo-controls[hidden] { + display: none; +} + +.demo-control, +.demo-dot { + appearance: none; + border: 1px solid var(--line-bright); + background: var(--panel-strong); + color: var(--text); + cursor: pointer; +} + +.demo-control { + display: grid; + width: 38px; + height: 38px; + padding: 0; + place-items: center; + font: inherit; + font-size: 1.25rem; + line-height: 1; +} + +.demo-control:hover:not(:disabled), +.demo-dot:hover { + border-color: var(--brand-2); + color: var(--brand-2); +} + +.demo-control:focus-visible, +.demo-dot:focus-visible { + outline: 2px solid var(--brand-2); + outline-offset: 3px; +} + +.demo-control:disabled { + cursor: not-allowed; + opacity: 0.35; +} + +.demo-dots { + display: flex; + align-items: center; + gap: 7px; +} + +.demo-dot { + width: 11px; + height: 11px; + padding: 0; + border-radius: 999px; + background: transparent; +} + +.demo-dot[aria-current="true"] { + border-color: var(--brand-2); + background: var(--brand-2); +} + +.demo-gallery { + display: flex; gap: 18px; + overflow-x: auto; + overscroll-behavior-x: contain; padding: 18px; + scroll-padding-inline: 18px; + scroll-snap-type: x mandatory; + scrollbar-color: var(--line-bright) transparent; + scrollbar-width: thin; + -webkit-overflow-scrolling: touch; +} + +.demo-gallery::-webkit-scrollbar { + height: 10px; +} + +.demo-gallery::-webkit-scrollbar-track { + background: transparent; +} + +.demo-gallery::-webkit-scrollbar-thumb { + border: 3px solid transparent; + background: var(--line-bright); + background-clip: content-box; +} + +.demo-gallery:focus-visible { + outline: 2px solid var(--brand-2); + outline-offset: -4px; } .demo-shot { display: grid; - gap: 10px; + flex: 1 0 calc((100% - 36px) / 3); + min-width: 286px; + gap: 12px; align-content: start; margin: 0; + scroll-snap-align: start; } -.demo-shot-desktop { - grid-row: span 2; -} - -.demo-shot img { +.demo-shot-media { + position: relative; + display: grid; overflow: hidden; - width: 100%; + aspect-ratio: 16 / 10; + place-items: center; + padding: 10px; border: 1px solid var(--line); - border-radius: 16px; - background: var(--panel-strong); + border-radius: var(--radius); + background: + radial-gradient(circle at 0 0, rgba(124, 60, 255, 0.22), transparent 34%), + var(--panel-strong); box-shadow: 0 18px 46px rgba(0, 0, 0, 0.2); } -.demo-shot-mobile img { - width: min(100%, 250px); - margin-inline: auto; +.demo-lightbox-trigger { + position: absolute; + inset: 0; + display: grid; + width: 100%; + height: 100%; + padding: 0; + place-items: center; + border: 0; + background: transparent; + color: inherit; + cursor: zoom-in; +} + +.demo-lightbox-trigger:focus-visible { + outline: 2px solid var(--brand-2); + outline-offset: -2px; +} + +.demo-shot img { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: contain; } .demo-shot figcaption { + display: grid; + gap: 4px; color: var(--muted-2); font-size: 0.9rem; line-height: 1.45; } +.demo-shot figcaption strong { + color: var(--text); + font-size: 0.94rem; +} + +.demo-lightbox { + width: min(1120px, calc(100vw - 28px)); + max-height: calc(100vh - 28px); + padding: 0; + border: 1px solid var(--line-bright); + background: var(--panel); + color: var(--text); +} + +.demo-lightbox::backdrop { + background: rgba(5, 7, 16, 0.78); + backdrop-filter: blur(6px); +} + +.demo-lightbox-panel { + position: relative; + display: grid; + gap: 12px; + max-height: calc(100vh - 28px); + padding: clamp(14px, 2vw, 22px); +} + +.demo-lightbox-panel img { + width: auto; + height: auto; + max-width: 100%; + max-height: calc(100vh - 116px); + justify-self: center; + border: 1px solid var(--line); + border-radius: var(--radius); + background: var(--panel-strong); + box-shadow: 0 18px 46px rgba(0, 0, 0, 0.28); +} + +.demo-lightbox-caption { + margin: 0; + color: var(--muted); + line-height: 1.5; + text-align: center; +} + +.demo-lightbox-close { + position: absolute; + top: 12px; + right: 12px; + z-index: 1; + display: grid; + width: 42px; + height: 42px; + padding: 0; + place-items: center; + border: 1px solid var(--line-bright); + background: var(--panel); + color: var(--text); + cursor: pointer; + font: inherit; + font-size: 1.35rem; + line-height: 1; +} + +.demo-lightbox-close:hover, +.demo-lightbox-close:focus-visible { + border-color: var(--brand-2); + color: var(--brand-2); +} + .manifesto-section { padding-top: 46px; } @@ -1169,12 +1372,8 @@ html[data-theme="light"] .comment { align-content: start; } - .demo-gallery { - grid-template-columns: 1fr; - } - - .demo-shot-desktop { - grid-row: auto; + .demo-shot { + flex-basis: min(58vw, 360px); } } @@ -1253,6 +1452,48 @@ html[data-theme="light"] .comment { font-size: clamp(3rem, 15vw, 4.2rem); } + .demo-caption { + align-items: flex-start; + } + + .demo-controls { + justify-content: space-between; + width: 100%; + margin-left: 0; + } + + .demo-dots { + flex: 1; + justify-content: center; + } + + .demo-gallery { + gap: 14px; + padding: 14px; + scroll-padding-inline: 14px; + } + + .demo-shot { + flex-basis: min(84vw, 340px); + min-width: 0; + } + + .demo-shot-media { + padding: 8px; + } + + .demo-lightbox { + width: calc(100vw - 20px); + } + + .demo-lightbox-panel { + padding: 12px; + } + + .demo-lightbox-panel img { + max-height: calc(100vh - 98px); + } + .footer-inner { align-items: flex-start; flex-direction: column;