Merge published comic changes and adapt navigation verification
Verify and deploy Tabularium / verify-and-deploy (pull_request) Failing after 5m28s

This commit is contained in:
2026-10-28 19:02:26 +08:00
19 changed files with 356 additions and 11 deletions
@@ -0,0 +1,66 @@
import { useId, useState } from "react";
import { ArchitectureFigure } from "./ArchitectureFigure";
type Slide = { source: string; alt: string };
export function FigureGallery({ slides }: { slides: Slide[] }) {
const [active, setActive] = useState(0);
const viewport = useId();
const slide = slides[active];
if (!slide) return null;
function move(offset: number) {
setActive((current) => (current + offset + slides.length) % slides.length);
}
return (
<section
className="figure-gallery"
role="region"
aria-roledescription="轮播"
aria-label="文档配图"
onKeyDown={(event) => {
if ((event.target as HTMLElement).closest("dialog")) return;
if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
event.preventDefault();
move(event.key === "ArrowLeft" ? -1 : 1);
}
}}
>
<div
id={viewport}
role="group"
aria-roledescription="幻灯片"
aria-label={`第 ${active + 1} 张,共 ${slides.length} 张`}
>
<ArchitectureFigure key={slide.source} source={slide.source} alt={slide.alt} />
</div>
<div className="figure-gallery-controls">
<button type="button" className="figure-gallery-arrow" aria-label="上一张图片" aria-controls={viewport} onClick={() => move(-1)}>
<span aria-hidden="true">‹</span>
</button>
<div className="figure-gallery-dots" role="group" aria-label="选择图片">
{slides.map((item, index) => (
<button
key={item.source}
type="button"
className="figure-gallery-dot"
aria-label={`显示第 ${index + 1} 张图片`}
aria-current={index === active ? "true" : undefined}
aria-controls={viewport}
onClick={() => setActive(index)}
>
<span aria-hidden="true" />
</button>
))}
</div>
<button type="button" className="figure-gallery-arrow" aria-label="下一张图片" aria-controls={viewport} onClick={() => move(1)}>
<span aria-hidden="true">›</span>
</button>
</div>
<p className="sr-only" role="status" aria-live="polite" aria-atomic="true">
第 {active + 1} 张,共 {slides.length} 张:{slide.alt}
</p>
</section>
);
}
+19 -3
View File
@@ -17,6 +17,7 @@ import remarkGfm from "remark-gfm";
import { listDocs, readDoc, type Doc, type DocLink } from "@/lib/docs";
import { Shell, Navigation } from "@/components/Shell";
import { ArchitectureFigure } from "@/components/ArchitectureFigure";
import { FigureGallery } from "@/components/FigureGallery";
import { CodeBlock } from "@/components/CodeBlock";
import { collectionEntries, collectionLabel } from "@/lib/collections";
import { Icon } from "@/components/Icon";
@@ -115,15 +116,30 @@ export default function DocPage({
}
return <a href={target}>{children}</a>;
},
p: ({ children, node }) =>
node?.children.some(
p: ({ children, node }) => {
const slides = node?.children.flatMap((child) =>
child.type === "element" && child.tagName === "img" &&
typeof child.properties.src === "string" &&
child.properties.src.startsWith("/figures/endor-")
? [{ source: child.properties.src, alt: String(child.properties.alt ?? "") }]
: [],
) ?? [];
const onlyImages = node?.children.every((child) =>
(child.type === "element" && child.tagName === "img" &&
typeof child.properties.src === "string" &&
child.properties.src.startsWith("/figures/endor-")) ||
(child.type === "text" && child.value.trim() === ""),
);
if (slides.length > 1 && onlyImages) return <FigureGallery slides={slides} />;
return node?.children.some(
(child) =>
child.type === "element" && child.tagName === "img",
) ? (
<div>{children}</div>
) : (
<p>{children}</p>
),
);
},
img: ({ src, alt }) =>
typeof src === "string" && src.startsWith("/figures/endor-") ? (
<ArchitectureFigure source={src} alt={alt ?? "Endor 架构图"} />
+51
View File
@@ -882,6 +882,57 @@ button {
border-radius: 0;
background: transparent;
}
.figure-gallery {
margin: 28px 0;
}
.figure-gallery .architecture-preview {
margin: 0;
}
.figure-gallery-controls,
.figure-gallery-dots {
display: flex;
align-items: center;
justify-content: center;
}
.figure-gallery-controls {
gap: 20px;
margin-top: 12px;
}
.figure-gallery-arrow,
.figure-gallery-dot {
display: grid;
place-items: center;
min-width: 36px;
min-height: 36px;
padding: 0;
border: 0;
border-radius: 50%;
color: #57534e;
background: transparent;
cursor: pointer;
}
.figure-gallery-arrow {
font-size: 28px;
line-height: 1;
}
.figure-gallery-dot span {
width: 7px;
height: 7px;
border-radius: 50%;
background: #d6d3d1;
}
.figure-gallery-dot[aria-current="true"] span {
background: #2563eb;
}
.figure-gallery-arrow:hover,
.figure-gallery-dot:hover {
background: #f5f5f4;
}
.figure-gallery-arrow:focus-visible,
.figure-gallery-dot:focus-visible {
outline: 2px solid #2563eb;
outline-offset: 2px;
}
.image-lightbox {
width: 100vw;
height: 100dvh;