Explain Endor concepts with a reproducible multi-region topology
Verify and deploy Tabularium / verify-and-deploy (push) Successful in 4m14s

This commit is contained in:
2026-10-27 17:45:30 +08:00
parent 75debc0068
commit 1c27f6bc1e
20 changed files with 1362 additions and 2 deletions
@@ -0,0 +1,105 @@
import { useRef, useState } from "react";
const source = "/figures/endor-architecture.svg";
export function ArchitectureFigure({ alt }: { alt: string }) {
const dialog = useRef<HTMLDialogElement>(null);
const trigger = useRef<HTMLButtonElement>(null);
const pan = useRef<HTMLDivElement>(null);
const [fit, setFit] = useState(true);
const [zoom, setZoom] = useState(1);
function open() {
setFit(true);
setZoom(1);
dialog.current?.showModal();
}
function changeZoom(delta: number) {
const current = fit
? ((pan.current?.clientWidth ?? 1776) - 16) / 1760
: zoom;
setFit(false);
setZoom(
Math.max(0.1, Math.min(2, Math.round((current + delta) * 100) / 100)),
);
}
return (
<figure className="architecture-figure">
<span className="figure-toolbar">
<span>ENDOR / 概念架构草案</span>
<button ref={trigger} onClick={open}>
放大查看 ↗
</button>
</span>
<button
className="figure-preview"
onClick={open}
aria-label="放大 Endor 架构图"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={source} alt={alt} />
</button>
<span className="figure-caption">
两区域拓扑 · 点击放大查看全连接与复制路径
</span>
<dialog
ref={dialog}
className="figure-dialog"
aria-label="Endor 架构图查看器"
onClose={() => trigger.current?.focus()}
>
<div className="figure-viewer-toolbar">
<strong>
Endor 概念架构 <small>草案</small>
</strong>
<div className="figure-controls">
<button
onClick={() => changeZoom(-0.25)}
aria-label="缩小架构图"
disabled={!fit && zoom <= 0.1}
>
−
</button>
<span aria-live="polite">
{fit ? "适应宽度" : `${Math.round(zoom * 100)}%`}
</span>
<button
onClick={() => changeZoom(0.25)}
aria-label="放大架构图"
disabled={!fit && zoom >= 2}
>
+
</button>
<button onClick={() => setFit(true)}>适应</button>
<a href={source} download="endor-architecture.svg">
下载 SVG
</a>
<button
onClick={() => dialog.current?.close()}
aria-label="关闭架构图"
>
关闭 ×
</button>
</div>
</div>
<div
ref={pan}
className="figure-pan"
tabIndex={0}
aria-label="架构图滚动区域,可使用方向键滚动"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={source}
alt={alt}
style={{ width: fit ? "100%" : 1760 * zoom }}
/>
</div>
<p className="figure-viewer-hint">
实线:请求 /
依赖 · 紫色:身份 · 蓝色点线:回源 · 金色:跨区备份。放大后可横向、纵向滚动,Esc
关闭。
</p>
</dialog>
</figure>
);
}
+1 -1
View File
@@ -116,7 +116,7 @@ export function Shell({
<Brand />
<nav className="top-nav" aria-label="主导航">
<Link href="/">文档概览</Link>
<Link href="/docs/02-mounting-draft">架构设计</Link>
<Link href="/docs/00-endor-concepts-draft">架构设计</Link>
<a href="https://gitea.entroia.com/Zayden-Jung/endorphin-tabularium-docs">
仓库 ↗
</a>
+7
View File
@@ -5,6 +5,13 @@ export type Section = {
symbol: string;
};
export function sectionFor(slug: string): Section {
if (slug.includes("endor-concepts"))
return {
group: "架构设计",
label: "概念草案",
symbol: "◇",
description: "从全球入口、区域运行时到对象数据,理解 Endor 的概念拓扑。",
};
if (slug.includes("mounting"))
return {
group: "架构设计",
+17
View File
@@ -16,6 +16,7 @@ import ReactMarkdown from "react-markdown";
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 { CodeBlock } from "@/components/CodeBlock";
import { headingsFor, sectionFor } from "@/lib/navigation";
@@ -102,6 +103,22 @@ export default function DocPage({
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
p: ({ children, node }) =>
node?.children.some(
(child) =>
child.type === "element" && child.tagName === "img",
) ? (
<div>{children}</div>
) : (
<p>{children}</p>
),
img: ({ src, alt }) =>
src === "/figures/endor-architecture.svg" ? (
<ArchitectureFigure alt={alt ?? "Endor 概念架构草案"} />
) : (
// eslint-disable-next-line @next/next/no-img-element
<img src={src} alt={alt ?? ""} />
),
table: ({ children }) => (
<div className="table-scroll">
<table>{children}</table>
+165 -1
View File
@@ -455,7 +455,7 @@ button {
}
.doc-cards {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 14px;
}
.doc-card {
@@ -1340,3 +1340,167 @@ button {
border: 0;
border-radius: 0;
}
/* The topology stays a reproducible SVG; React only supplies the viewer. */
.architecture-figure {
display: block;
border: 1px solid var(--line);
border-radius: 10px;
overflow: hidden;
margin: 28px 0;
}
.figure-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px 16px;
background: #f5f8f3;
font:
11px ui-monospace,
monospace;
color: #6c806f;
}
.figure-toolbar button {
color: var(--green);
border: 1px solid #d7e3d4;
padding: 5px 10px;
border-radius: 5px;
background: white;
}
.figure-preview {
display: block;
width: 100%;
background: #fcfdfb;
cursor: zoom-in;
}
.prose .figure-preview img {
display: block;
width: 100%;
padding: 0;
margin: 0;
border: 0;
border-radius: 0;
}
.figure-caption {
display: block;
padding: 10px 16px;
font-size: 11px;
color: #758576;
border-top: 1px solid var(--line);
}
.figure-dialog {
padding: 0;
width: 96vw;
max-width: 1900px;
height: 94dvh;
max-height: 94dvh;
border: 1px solid #dce5d9;
border-radius: 12px;
margin: auto;
overflow: hidden;
background: #fcfdfb;
box-shadow: 0 20px 100px #18312530;
}
.figure-dialog::backdrop {
background: #1e322e66;
backdrop-filter: blur(4px);
}
.figure-dialog[open] {
display: flex;
flex-direction: column;
}
.figure-viewer-toolbar {
display: flex;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
padding: 12px 18px;
background: #fff;
border-bottom: 1px solid var(--line);
align-items: center;
}
.figure-viewer-toolbar strong {
font-size: 14px;
font-weight: 600;
color: var(--ink);
}
.figure-viewer-toolbar small {
color: #a68b52;
font-size: 11px;
margin-left: 5px;
}
.figure-controls {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
font-size: 11px;
color: #647c67;
}
.figure-controls button,
.figure-controls a {
min-height: 32px;
min-width: 32px;
padding: 4px 9px;
border: 1px solid #dae4d6;
border-radius: 5px;
background: #fff;
font-weight: 400;
text-decoration: none;
}
.figure-controls button:disabled {
opacity: 0.35;
cursor: default;
}
.figure-controls > span {
min-width: 62px;
text-align: center;
}
.figure-pan {
overflow: auto;
flex: 1;
min-height: 0;
overscroll-behavior: contain;
padding: 8px;
}
.prose .figure-pan img {
display: block;
max-width: none;
height: auto;
margin: 0 auto;
padding: 0;
border: 0;
border-radius: 0;
background: #fcfdfb;
}
.figure-viewer-hint {
margin: 0 !important;
padding: 10px 18px;
border-top: 1px solid var(--line);
background: #fff;
color: #798d79;
font-size: 10px !important;
line-height: 1.7 !important;
}
@media (max-width: 560px) {
.figure-viewer-toolbar {
padding: 10px;
gap: 8px;
}
.figure-controls {
gap: 5px;
}
.figure-controls button,
.figure-controls a {
min-height: 36px;
}
.figure-toolbar {
padding: 10px;
font-size: 10px;
}
.figure-caption {
font-size: 10px;
padding: 10px;
}
}