T48a Docs · Frame: a Docs tab, /docs/ and /docs/$page/, Markdown guides with a rail, an on-this-page list, previous and next and code blocks; full Rust samples are compiled examples shown as text

This commit is contained in:
ZacharyZhang-NY committed 2026-10-01 11:40:50 -04:00
1 parent 157176e2fb
commit fba90c22e6
12 files changed
+305 -6

No files matched your search

+1
View File
@@ -27,6 +27,7 @@
"@fontsource-variable/geist-mono": "5.3.0",
"@tanstack/react-router": "1.170.40",
"lenis": "1.3.26",
"marked": "18.0.14",
"react": "19.3.0",
"react-dom": "19.3.0",
"reicon": "1.2.4"
+10
View File
@@ -20,6 +20,9 @@ importers:
lenis:
specifier: 1.3.26
version: 1.3.26(react@19.3.0)
marked:
specifier: 18.0.14
version: 18.0.14
react:
specifier: 19.3.0
version: 19.3.0
@@ -870,6 +873,11 @@ packages:
resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==}
engines: {node: '>= 12.0.0'}
marked@18.0.14:
resolution: {integrity: sha512-mBHK6FBHuBAlhgRe88w9F0O1AbwwXJUcQibUbC/QcdTbVGAD7aWza+xt3N6oT/jCZx3/OMeS+8rnuiHZcQ9s7A==}
engines: {node: '>= 20'}
hasBin: true
miniflare@5.20260926.0-alpha:
resolution: {integrity: sha512-m/M3rpLnaouw6QSoKzylfw6Z1dnOwRM48idnZ7kc5ivRqBXANFQ8XqKCyQ/V+xqQ9Rg1L99j4WkZGVU9uT4L2w==}
engines: {node: '>=22.0.0'}
@@ -1570,6 +1578,8 @@ snapshots:
lightningcss-win32-arm64-msvc: 1.33.0
lightningcss-win32-x64-msvc: 1.33.0
marked@18.0.14: {}
miniflare@5.20260926.0-alpha(@types/node@24.19.0):
dependencies:
'@cspotcode/source-map-support': 0.8.1
+4 -1
View File
@@ -20,9 +20,12 @@ function Header() {
<Link to="/components/" className="link" activeProps={{ "aria-current": "page" }}>
Components
</Link>
<Link to="/docs/" className="link" activeProps={{ "aria-current": "page" }}>
Docs
</Link>
</nav>
<div className="end">
<button className="tool" aria-label={sound ? "Sound off" : "Sound on"} aria-pressed={sound} onClick={() => setSound(!sound)}>
<button className="tool sound" aria-label={sound ? "Sound off" : "Sound on"} aria-pressed={sound} onClick={() => setSound(!sound)}>
<Icon name={sound ? "Sound" : "VolumeCross"} />
</button>
<button className="tool" aria-label={dark ? "Light mode" : "Dark mode"} onClick={() => setMode(dark ? "light" : "dark")}>
+67
View File
@@ -0,0 +1,67 @@
import { Marked, type Tokens } from "marked";
const sources = import.meta.glob<string>("./*.md", { query: "?raw", import: "default", eager: true });
const examples = import.meta.glob<string>("../../../examples/docs/*.rs", { query: "?raw", import: "default", eager: true });
/** The guides in reading order, by file name. */
const ORDER = ["introduction"];
export interface Guide {
/** The path segment; the introduction has none. */
slug: string;
title: string;
html: string;
/** The `##` headings, for the on-this-page list. */
toc: { id: string; text: string }[];
}
const escape = (text: string) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
const slugify = (text: string) =>
text
.toLowerCase()
.replace(/<[^>]+>/g, "")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "");
function read(file: string): Guide {
const source = sources[`./${file}.md`];
if (source === undefined) throw new Error(`docs: no ${file}.md`);
let title = "";
const toc: Guide["toc"] = [];
const marked = new Marked({
renderer: {
heading({ tokens, depth }: Tokens.Heading) {
const html = this.parser.parseInline(tokens);
if (depth === 1) {
title = html.replace(/<[^>]+>/g, "");
return "";
}
const id = slugify(html);
if (depth === 2) {
if (toc.some((h) => h.id === id)) throw new Error(`docs: ${file}.md repeats heading ${id}`);
toc.push({ id, text: html.replace(/<[^>]+>/g, "") });
}
return `<h${depth} id="${id}">${html}</h${depth}>`;
},
code({ text, lang }: Tokens.Code) {
const [language = "", ...rest] = (lang ?? "").split(/\s+/);
const example = rest.find((part) => part.startsWith("example="))?.slice("example=".length);
let code = text;
let caption = language;
if (example) {
const body = examples[`../../../examples/docs/${example}.rs`];
if (body === undefined) throw new Error(`docs: ${file}.md names a missing example ${example}`);
code = body.trimEnd();
caption = `examples/docs/${example}.rs · cargo run --example docs_${example}`;
}
return `<figure class="code"><figcaption>${escape(caption)}</figcaption><button class="copy" type="button">Copy</button><pre><code>${escape(code)}</code></pre></figure>`;
},
},
});
const html = marked.parse(source, { async: false });
if (!title) throw new Error(`docs: ${file}.md has no # title`);
return { slug: file === "introduction" ? "" : file, title, html, toc };
}
export const guides: Guide[] = ORDER.map(read);
+14
View File
@@ -0,0 +1,14 @@
# Introduction
Ely is a component library for [GPUI](https://www.gpui.rs), the Rust UI framework behind Zed. Every component draws in light and dark, and every one runs live on the [components page](/components/).
## A first window
The example opens a window with one counter and one Button. `cargo run --example docs_hello` runs it from a clone of the repository.
```rust example=hello
```
## License
MIT or Apache-2.0, at your option.
+4 -1
View File
@@ -5,6 +5,7 @@ import { createRoot } from "react-dom/client";
import { Shell } from "./Shell";
import { chapters } from "./data";
import { Chapters } from "./pages/Chapters";
import { Docs } from "./pages/Docs";
import { Home } from "./pages/Home";
import { NotFound } from "./pages/NotFound";
import { Story } from "./pages/Story";
@@ -24,9 +25,11 @@ const chapter = createRoute({
component: () => <NotFound what="No chapter by that name." />,
});
const story = createRoute({ getParentRoute: () => root, path: "/components/$page/$story", component: Story });
const docs = createRoute({ getParentRoute: () => root, path: "/docs", component: Docs });
const doc = createRoute({ getParentRoute: () => root, path: "/docs/$page", component: Docs });
const router = createRouter({
routeTree: root.addChildren([home, index, chapter, story]),
routeTree: root.addChildren([home, index, chapter, story, docs, doc]),
trailingSlash: "always",
scrollRestoration: true,
});
+81
View File
@@ -0,0 +1,81 @@
import { Link, useNavigate, useParams } from "@tanstack/react-router";
import { useEffect, type MouseEvent } from "react";
import { guides, type Guide } from "../docs/guides";
import { NotFound } from "./NotFound";
const to = (guide: Guide) => (guide.slug ? `/docs/${guide.slug}/` : "/docs/");
async function copy(button: HTMLButtonElement) {
const code = button.parentElement?.querySelector("code")?.textContent;
if (code == null) throw new Error("docs: a copy button without code");
try {
await navigator.clipboard.writeText(code);
button.textContent = "Copied";
} catch (error) {
console.error("docs: copy failed", error);
button.textContent = "Copy failed";
}
setTimeout(() => (button.textContent = "Copy"), 1600);
}
export function Docs() {
const { page } = useParams({ strict: false });
const navigate = useNavigate();
const at = guides.findIndex((g) => g.slug === (page ?? ""));
const guide = guides[at];
useEffect(() => {
if (guide) document.title = `${guide.title} · Docs · Ely`;
}, [guide]);
if (!guide) return <NotFound what={`No guide at ${page}.`} />;
const [prev, next] = [guides[at - 1], guides[at + 1]];
// Links and copy buttons inside the rendered Markdown.
const press = (e: MouseEvent<HTMLElement>) => {
const target = e.target as HTMLElement;
const button = target.closest<HTMLButtonElement>("button.copy");
if (button) return void copy(button);
const link = target.closest<HTMLAnchorElement>("a[href^='/']");
if (!link || e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;
e.preventDefault();
navigate({ to: link.getAttribute("href")! });
};
return (
<div className="docs">
<nav className="docs-rail" aria-label="Guides" data-lenis-prevent>
<ol>
{guides.map((g) => (
<li key={g.slug}>
<Link to={to(g)} aria-current={g === guide ? "page" : undefined}>
{g.title}
</Link>
</li>
))}
</ol>
</nav>
<article className="doc">
<h1>{guide.title}</h1>
<div className="prose" onClick={press} dangerouslySetInnerHTML={{ __html: guide.html }} />
<nav className="pager" aria-label="More guides">
{prev && <Link to={to(prev)}>← {prev.title}</Link>}
{next && (
<Link className="next" to={to(next)}>
{next.title} →
</Link>
)}
</nav>
</article>
{guide.toc.length > 0 && (
<nav className="doc-toc" aria-label="On this page">
<ol>
{guide.toc.map((h) => (
<li key={h.id}>
<a href={`#${h.id}`}>{h.text}</a>
</li>
))}
</ol>
</nav>
)}
</div>
);
}
+48 -1
View File
@@ -9,6 +9,7 @@
--iso-line: #1a1918;
--iso-side: #f3f0ec;
--iso-shade: #e9e5df;
--code: #f3f0ec;
--sans: "Geist Variable", system-ui, sans-serif;
--mono: "Geist Mono Variable", ui-monospace, monospace;
color-scheme: light;
@@ -25,6 +26,7 @@
--iso-line: #d9d5d0;
--iso-side: #1a1816;
--iso-shade: #201e1c;
--code: #1b1917;
color-scheme: dark;
}
@@ -162,13 +164,51 @@ img.shot { display: block; max-width: 100%; height: auto; }
.pager a:hover { color: var(--fg); }
.pager .next { margin-left: auto; text-align: right; }
.docs { display: grid; grid-template-columns: 200px minmax(0, 720px) 200px; justify-content: center; gap: 64px; padding: 0 64px; }
.docs-rail, .doc-toc { position: sticky; top: 64px; height: calc(100vh - 64px); overflow-y: auto; padding: 96px 0 64px; }
.docs-rail ol, .doc-toc ol { list-style: none; margin: 0; padding: 0; }
.docs-rail li { padding: 5px 0; font-size: 15px; }
.doc-toc li { padding: 4px 0; font-size: 13px; line-height: 20px; }
.docs-rail a, .doc-toc a { color: var(--muted); transition: color 160ms ease-out; }
.doc-toc a { color: var(--faint); }
.docs-rail a:hover, .doc-toc a:hover, .docs-rail a[aria-current="page"] { color: var(--fg); }
.docs-rail a[aria-current="page"] { font-weight: 500; }
.doc { padding: 96px 0 128px; min-width: 0; }
.doc h1 { font-size: clamp(36px, 4vw, 56px); line-height: 1.05; font-weight: 800; letter-spacing: -0.035em; margin-bottom: 40px; }
.prose { font-size: 17px; line-height: 30px; }
.prose p, .prose li { color: color-mix(in srgb, var(--fg) 82%, var(--bg)); }
.prose p { margin: 0 0 20px; }
.prose h2 { font-size: 28px; line-height: 36px; font-weight: 700; letter-spacing: -0.02em; margin: 64px 0 16px; scroll-margin-top: 96px; }
.prose h3 { font-size: 20px; line-height: 28px; font-weight: 600; margin: 40px 0 12px; scroll-margin-top: 96px; }
.prose a { color: var(--fg); text-decoration: underline; text-underline-offset: 3px; text-decoration-color: var(--line); }
.prose a:hover { text-decoration-color: var(--fg); }
.prose ul, .prose ol { margin: 0 0 20px; padding-left: 22px; }
.prose code { font: 400 0.86em/1 var(--mono); background: var(--code); padding: 3px 6px; border-radius: 6px; }
.prose table { width: 100%; border-collapse: collapse; margin: 8px 0 28px; font-size: 15px; line-height: 24px; }
.prose th, .prose td { text-align: left; vertical-align: top; padding: 12px 16px 12px 0; border-top: 1px solid var(--line); }
.prose th { font-weight: 600; }
.prose td { color: color-mix(in srgb, var(--fg) 82%, var(--bg)); }
.code { position: relative; margin: 28px 0; background: var(--code); border-radius: 12px; }
.code figcaption { padding: 14px 88px 0 20px; font: 400 12px/18px var(--mono); color: var(--faint); overflow-wrap: anywhere; }
.code pre { margin: 0; padding: 14px 20px 20px; overflow-x: auto; }
.code code { display: block; font: 400 13.5px/22px var(--mono); background: none; padding: 0; color: var(--fg); }
.code .copy { position: absolute; top: 10px; right: 12px; padding: 4px 8px; font-size: 12px; color: var(--muted); transition: color 160ms ease-out; }
.code .copy:hover { color: var(--fg); }
.missing { min-height: 70vh; display: grid; align-content: center; justify-items: start; gap: 24px; padding: 160px 64px; }
.missing h1 { font-size: 64px; letter-spacing: -0.04em; }
.foot { display: flex; flex-wrap: wrap; gap: 8px 28px; padding: 48px 64px; font-size: 13px; color: var(--faint); }
.foot a { text-decoration: underline; text-underline-offset: 3px; }
@media (max-width: 1279px) {
.docs { grid-template-columns: 200px minmax(0, 720px); }
.doc-toc { display: none; }
}
@media (max-width: 1023px) {
.docs { grid-template-columns: minmax(0, 1fr); }
.docs-rail { display: none; }
.reader { grid-template-columns: minmax(0, 1fr); }
.side { display: none; }
.feature, .feature:nth-of-type(odd), .city { grid-template-columns: minmax(0, 1fr); grid-template-rows: none; gap: 32px; }
@@ -182,7 +222,7 @@ img.shot { display: block; max-width: 100%; height: auto; }
.bar { padding: 0 16px; gap: 12px; }
.bar .end { gap: 0; }
.hero, .chapters, .missing { padding-left: 20px; padding-right: 20px; }
.reader { padding: 0 20px; }
.reader, .docs { padding: 0 20px; }
.chapters li { grid-template-columns: 40px minmax(0, 1fr); }
.chapters .titles { grid-column: 2; margin-top: 8px; }
.foot { padding: 32px 20px; }
@@ -199,6 +239,13 @@ img.shot { display: block; max-width: 100%; height: auto; }
.close { padding: 112px 20px; }
}
@media (max-width: 359px) {
.bar { padding: 0 12px; gap: 10px; }
.bar nav { gap: 14px; }
.bar .tool { width: 32px; }
.bar .sound { display: none; }
}
@media (prefers-reduced-motion: reduce) {
* { transition: none !important; animation: none !important; }
}