+
-
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.
+
+
-
-
-
- Desktop: chat beside workspace file preview.
+
+
+
+ Desktop Chat beside workspace file preview.
-
-
- Tablet: the same session from a wider touch screen.
+
+
+ Tablet The same live session on a wider touch screen.
-
-
- Mobile: the chat stays readable on the go.
+
+
+ Mobile Readable chat controls when you are away from the desk.
+
+
+
×
+
+
+
+
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;