Add a compressed illustration carousel with slide indicators
Verify and deploy Tabularium / verify-and-deploy (pull_request) Failing after 5m44s

This commit is contained in:
2026-10-28 18:16:17 +08:00
parent c08371647d
commit bd932c36b2
16 changed files with 191 additions and 17 deletions
+1
View File
@@ -6,4 +6,5 @@
**/*.pyc
**/*.tsbuildinfo
.artifacts
apps/docs/public/figures/**/*.webp
.env*
+3
View File
@@ -11,3 +11,6 @@ __pycache__/
!.env.example
.artifacts/
# Browser raster derivatives are generated on the confirmed remote runner.
/apps/docs/public/figures/**/*.webp
Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

+20 -8
View File
@@ -142,6 +142,7 @@ async function main() {
"/docs/00-endor-concepts-draft",
"/figures/endor-architecture.svg",
"/figures/endor-review-gate.webp",
"/figures/endor-review-desk.webp",
"/docs/pretext/01-chapter",
"/materials/pretext/verify.cjs",
"/materials/pretext/results.chrome156.json",
@@ -200,15 +201,26 @@ async function main() {
2,
"The comic preview and lightbox must use WebP",
);
assert.ok(!purpose.includes("endor-review-gate.png"));
const comic = await request("/figures/endor-review-gate.webp", { headers });
assert.equal(comic.status, 200);
assert.match(comic.headers.get("content-type") || "", /image\/webp/);
assert.deepEqual(
Buffer.from(await comic.arrayBuffer()),
readFileSync(join(__dirname, "public/figures/endor-review-gate.webp")),
assert.ok(purpose.includes('aria-label="上一张图片"'));
assert.ok(purpose.includes('aria-label="下一张图片"'));
assert.ok(purpose.includes('aria-label="显示第 1 张图片" aria-current="true"'));
assert.ok(purpose.includes('aria-label="显示第 2 张图片"'));
assert.equal(
[...purpose.matchAll(/<img\b[^>]*\bsrc="\/figures\/endor-review-desk\.webp"/g)].length,
0,
"The second slide should not download until selected",
);
assert.equal((await request("/figures/endor-review-gate.png", { headers })).status, 404);
for (const name of ["endor-review-gate", "endor-review-desk"]) {
assert.ok(!purpose.includes(`/figures/${name}.png`));
const comic = await request(`/figures/${name}.webp`, { headers });
assert.equal(comic.status, 200);
assert.match(comic.headers.get("content-type") || "", /image\/webp/);
assert.deepEqual(
Buffer.from(await comic.arrayBuffer()),
readFileSync(join(__dirname, `public/figures/${name}.webp`)),
);
assert.equal((await request(`/figures/${name}.png`, { headers })).status, 404);
}
const assets = new Set(
[...home.matchAll(/(?:src|href)="(\/_next\/static\/[^" ]+)"/g)].map((m) =>
m[1].replaceAll("&amp;", "&"),
+2 -2
View File
@@ -52,8 +52,8 @@ for (const source of await files(sources)) {
throw new Error(`${relative}: ${encoded.length} bytes exceeds the ${policy.maxBytes}-byte image budget.`);
}
if (check) {
const committed = await readFile(destination);
if (!encoded.equals(committed)) throw new Error(`${relative} is stale. Run pnpm --filter @endor/docs images and commit the output.`);
const generated = await readFile(destination);
if (!encoded.equals(generated)) throw new Error(`${relative} is stale. Regenerate browser images on the confirmed remote runner.`);
} else {
await mkdir(path.dirname(destination), { recursive: true });
await writeFile(destination, encoded);
@@ -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";
@@ -102,15 +103,30 @@ export default function DocPage({
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
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;
+7 -1
View File
@@ -33,4 +33,10 @@ pnpm --filter @endor/docs test:images
原图相对 `tools/figures/assets` 的路径会映射到 `apps/docs/public/figures` 下同名的 `.webp`。例如 `endor-review-gate.png` 对应 `/figures/endor-review-gate.webp`。文档只嵌入后一个地址,制作原稿不随网页发布。
开发启动和生产构建会运行压缩脚本;CI 还会重新编码并核对提交的 WebP,防止修改原稿或策略后遗漏更新产物。检查同时拒绝公开目录里的原始位图及 Markdown 图片或 HTML img 直接引用的原始 PNG、JPEG。SVG 无需转成位图。新增位图时一起提交原稿、生成的 WebP 与制作记录,沿用共同策略。
远程生产构建会运行压缩脚本;CI 先生成 WebP,再重新编码核对产物与原稿、策略是否一致。WebP 是构建派生文件,不必在本机生成或提交;Git 保存原稿、制作记录与共享策略。检查同时拒绝公开目录里的原始位图及 Markdown 图片或 HTML img 直接引用的原始 PNG、JPEG。SVG 无需转成位图,仍按前述流程提交可编辑输出。
## 同一位置切换多张配图
把两张或更多 `/figures/endor-` 图片写在同一段,中间换行但不插入空行,正文会将它们呈现为图组。左右按钮和键盘左右方向键可以切换,下方圆点表示当前图片,也可点击直接选择。图片保留点击放大能力。
图组一次只挂载当前图片,切换后才加载下一张压缩图,不预先下载整组原图。
+3
View File
@@ -19,6 +19,9 @@
## 让产出成为检查下一次产出的帮手
![Coding Agent 不断产出相同的原始文件:上方待检查文件积累为积分 dW,单人的检查门保持固定能力 C₀;下方 Endor 只把本次新增文件 dW 送到人与检查门前,处理完成的文件变成结构化蓝色帮手,箭头回到检查门,表示历史成果积累为新的检查能力](/figures/endor-review-gate.webp)
![检查台版:上方单人检查台面对积累的待处理文件;下方只检查本次新增文件,已处理文件变成蓝色帮手,向检查台贡献更多检查工具](/figures/endor-review-desk.webp)
两张漫画用检查门与检查台表达同一个目标,可用左右按钮或下方圆点切换。
上方把 Coding Agent 比作一台不断吐出工作结果的机器。机器可以持续产出,但人的时间和精力没有同步增长;等待检查的材料越积越多。虚线圈出的 **∫dW** 表示累积的待处理工作,门上的 **C₀** 表示在这个对比中保持不变的检查能力。通过门、带上勾的文件才表示已处理的结果。
+1 -1
View File
@@ -13,7 +13,7 @@
"lint": "pnpm --filter @endor/docs run lint",
"typecheck": "pnpm --filter @endor/docs run typecheck",
"figures": "uv run --locked --project tools/figures endor-figures",
"check": "pnpm run lint && pnpm run typecheck && pnpm run test:auth && pnpm --filter @endor/docs run test:images && pnpm run figures && pnpm run build",
"check": "pnpm run lint && pnpm run typecheck && pnpm run test:auth && pnpm --filter @endor/docs run images && pnpm --filter @endor/docs run test:images && pnpm run figures && pnpm run build",
"test:auth": "pnpm --filter @endor/docs run test:auth"
}
}
+1
View File
@@ -6,6 +6,7 @@ uv sync --project tools/figures --locked
pnpm run lint
pnpm run typecheck
pnpm --filter @endor/docs run test:auth
pnpm --filter @endor/docs run images
pnpm --filter @endor/docs run test:images
cp -R apps/docs/public/figures /tmp/expected-figures
pnpm run figures
+1 -1
View File
@@ -27,7 +27,7 @@ def verify(base: str) -> None:
assert status == 200 and json.loads(health) == {"status": "ok"}
status, _, login = get("/login")
assert status == 200 and "输入动态码" in login.decode()
for path in ("/", "/docs/00-endor-concepts-draft", "/figures/endor-architecture.svg", "/figures/endor-review-gate.webp", "/docs/pretext/01-chapter", "/materials/pretext/verify.cjs", "/materials/pretext/results.chrome156.json"):
for path in ("/", "/docs/00-endor-concepts-draft", "/figures/endor-architecture.svg", "/figures/endor-review-gate.webp", "/figures/endor-review-desk.webp", "/docs/pretext/01-chapter", "/materials/pretext/verify.cjs", "/materials/pretext/results.chrome156.json"):
status, headers, _ = get(path)
assert status == 307, path
target = urllib.parse.urlparse(urllib.parse.urljoin(base, headers["Location"]))
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

@@ -0,0 +1,14 @@
Endor inspection desk comic — approved second slide
Source: built-in image_gen; physics inspection-bench variant with corrected top-row flow.
Original: exec-432e0f3d-d01d-4658-b5d5-0c63db95e472.png
The user selected this alongside the gate variant for one carousel.
The two identical machines emit raw papers. Above, a dashed integral dW boundary
encloses the backlog BEFORE one person's fixed-capacity C0 inspection desk.
Below, a dashed dW boundary encloses the one new paper being inspected. Earlier
checked outputs become structured document helpers that contribute additional
magnifiers to the Endor desk, whose capacity is C0 + k integral dW.
Store this PNG as the production master outside public. The confirmed remote
runner generates /figures/endor-review-desk.webp using content/image-policy.json.
+2 -1
View File
@@ -3,7 +3,8 @@ Endor review gate comic — approved source
Source: built-in image_gen, physics gate variant, approved by the user on 2026-10-28.
Original: exec-380f8db3-6450-4981-97df-473c13a62223.png
The PNG is the production master, not a public browser asset.
Generate the browser WebP with: pnpm --filter @endor/docs images
The confirmed remote runner generates the browser WebP with:
pnpm --filter @endor/docs images
Visual specification:
Two rows, white background, charcoal outlines, blue accents, identical coding