T48a Docs · Frame: guides render at build in a Vite plugin, so a bad one fails the build; focus rings whole in the rail and list; plain heading words, unique ids, captions only when named, code under example= refused
This commit is contained in:
9 files changed
+71
-26
No files matched your search
@@ -17,7 +17,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
|
||||
- Pictures: `image` 0.25 (MIT OR Apache-2.0), the crate gpui decodes with, default features off; it turns decoded frames and rims map tiles.
|
||||
- Maps: Natural Earth's 110m countries (public domain). `assets/maps/countries.geojson`, trimmed by `scripts/countries.py` to an ISO code, a name and positions to 0.01°, backs `maps::WorldMap`; the gallery's tiles are cut from the same file, z0 to z3, by `scripts/tiles.py` with Pillow.
|
||||
- Web: on wasm32, `web-time` 1.1 (MIT OR Apache-2.0) gives the `Instant` gpui's executor returns, std's own natively; jiff takes `js`, rust-embed `debug-embed`, similar `wasm32_web_time`. The gallery's web entry takes `gpui_web`, `wasm-bindgen`, `js-sys` and `web-sys` as wasm-only dev-dependencies, and a Hebrew cut of Noto Sans Hebrew 2.003 (OFL, `examples/gallery/fonts`): gpui_web asks the browser for emoji and CJK alone. The `wasm-bindgen` CLI at Cargo.lock's version and binaryen's `wasm-opt` finish the bundle.
|
||||
- Gallery: `examples/gallery`. Website: `frontend/` (Vite 8.3, pnpm 11, TypeScript, React 19.3, TanStack Router 1.170 in code-based routes, Lenis 1.3), in serro.ai's language: heavy titles over thin faint text, hairlines, one accent, full-screen sections, isometric line miniatures on a canvas. Pages name a story by its title's first part (`src/data.ts`'s `name`); scroll bars stay hidden; layouts step at 1024 and 768. Each story runs live from the gallery built for wasm32 through gpui_web, in a same-origin iframe, or, where a browser cannot run it, as a native capture. The Docs tab is Markdown in `frontend/src/docs/`, read in its `ORDER` and rendered at build by `marked` 18 (MIT); a fenced block tagged `example=NAME` shows `examples/docs/NAME.rs`, an example `scripts/check.sh` compiles as `docs_NAME`, so every full Rust sample on the site builds. Below 360px the header drops the sound switch. Geist and Geist Mono (OFL, `@fontsource-variable`), Reicon icons (MIT, based on Solar Icons, CC BY 4.0, credited in the footer), simple-icons' GitHub mark (CC0). It deploys to Cloudflare Workers static assets through `npx wrangler` (4.143).
|
||||
- Gallery: `examples/gallery`. Website: `frontend/` (Vite 8.3, pnpm 11, TypeScript, React 19.3, TanStack Router 1.170 in code-based routes, Lenis 1.3), in serro.ai's language: heavy titles over thin faint text, hairlines, one accent, full-screen sections, isometric line miniatures on a canvas. Pages name a story by its title's first part (`src/data.ts`'s `name`); scroll bars stay hidden; layouts step at 1024 and 768. Each story runs live from the gallery built for wasm32 through gpui_web, in a same-origin iframe, or, where a browser cannot run it, as a native capture. The Docs tab is Markdown in `frontend/src/docs/`, read in its `ORDER` and rendered by `marked` 18 (MIT) inside `vite.config.ts`'s `docs` plugin, so a bad guide fails `pnpm build` and the browser gets HTML alone; a fenced block tagged `example=NAME` shows `examples/docs/NAME.rs`, an example `scripts/check.sh` compiles as `docs_NAME`, so every full Rust sample on the site builds. Below 360px the header drops the sound switch. Geist and Geist Mono (OFL, `@fontsource-variable`), Reicon icons (MIT, based on Solar Icons, CC BY 4.0, credited in the footer), simple-icons' GitHub mark (CC0). It deploys to Cloudflare Workers static assets through `npx wrangler` (4.143).
|
||||
|
||||
## Commands
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"@types/react": "19.3.0",
|
||||
"@types/react-dom": "19.3.0",
|
||||
"@vitejs/plugin-react": "6.1.1",
|
||||
"marked": "18.0.14",
|
||||
"typescript": "^7.0.2",
|
||||
"vite": "8.3.1",
|
||||
"wrangler": "4.143.0"
|
||||
@@ -27,7 +28,6 @@
|
||||
"@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"
|
||||
|
||||
Generated
+3
-3
@@ -20,9 +20,6 @@ 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
|
||||
@@ -48,6 +45,9 @@ importers:
|
||||
'@vitejs/plugin-react':
|
||||
specifier: 6.1.1
|
||||
version: 6.1.1(vite@8.3.1(@types/node@24.19.0)(esbuild@0.28.1))
|
||||
marked:
|
||||
specifier: 18.0.14
|
||||
version: 18.0.14
|
||||
typescript:
|
||||
specifier: ^7.0.2
|
||||
version: 7.0.2
|
||||
|
||||
+29
-16
@@ -1,7 +1,9 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
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 });
|
||||
// Runs in Vite's config at build time, never in the browser.
|
||||
const at = (relative: string) => fileURLToPath(new URL(relative, import.meta.url));
|
||||
|
||||
/** The guides in reading order, by file name. */
|
||||
const ORDER = ["introduction"];
|
||||
@@ -23,14 +25,20 @@ const slugify = (text: string) =>
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
|
||||
/** A heading's words: no backticks, links as their text. */
|
||||
const plain = (markdown: string) => markdown.replace(/`/g, "").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1");
|
||||
/** A heading's words: no code, emphasis or link marks. */
|
||||
const plain = (markdown: string) =>
|
||||
markdown
|
||||
.replace(/`/g, "")
|
||||
.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
|
||||
.replace(/(\*\*|\*)([^*]+)\1/g, "$2");
|
||||
|
||||
function read(file: string): Guide {
|
||||
const source = sources[`./${file}.md`];
|
||||
if (source === undefined) throw new Error(`docs: no ${file}.md`);
|
||||
function read(file: string, files: string[]): Guide {
|
||||
const path = at(`./${file}.md`);
|
||||
files.push(path);
|
||||
const source = readFileSync(path, "utf8");
|
||||
let title = "";
|
||||
const toc: Guide["toc"] = [];
|
||||
const ids = new Set<string>();
|
||||
const marked = new Marked({
|
||||
renderer: {
|
||||
heading({ tokens, depth, text }: Tokens.Heading) {
|
||||
@@ -42,10 +50,9 @@ function read(file: string): Guide {
|
||||
}
|
||||
const html = this.parser.parseInline(tokens);
|
||||
const id = slugify(words);
|
||||
if (depth === 2) {
|
||||
if (toc.some((h) => h.id === id)) throw new Error(`docs: ${file}.md repeats heading ${id}`);
|
||||
toc.push({ id, text: words });
|
||||
}
|
||||
if (ids.has(id)) throw new Error(`docs: ${file}.md repeats heading ${id}`);
|
||||
ids.add(id);
|
||||
if (depth === 2) toc.push({ id, text: words });
|
||||
return `<h${depth} id="${id}">${html}</h${depth}>`;
|
||||
},
|
||||
code({ text, lang }: Tokens.Code) {
|
||||
@@ -54,12 +61,14 @@ function read(file: string): Guide {
|
||||
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();
|
||||
if (text.trim()) throw new Error(`docs: ${file}.md puts code under example=${example}`);
|
||||
const path = at(`../../../examples/docs/${example}.rs`);
|
||||
files.push(path);
|
||||
code = readFileSync(path, "utf8").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 head = caption ? `<figcaption>${escape(caption)}</figcaption>` : "";
|
||||
return `<figure class="code">${head}<button class="copy" type="button">Copy</button><pre><code>${escape(code)}</code></pre></figure>`;
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -68,4 +77,8 @@ function read(file: string): Guide {
|
||||
return { slug: file === "introduction" ? "" : file, title, html, toc };
|
||||
}
|
||||
|
||||
export const guides: Guide[] = ORDER.map(read);
|
||||
/** Every guide, and every file read to make them. */
|
||||
export function render(): { guides: Guide[]; files: string[] } {
|
||||
const files: string[] = [];
|
||||
return { guides: ORDER.map((file) => read(file, files)), files };
|
||||
}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
declare module "virtual:guides" {
|
||||
export const guides: import("./guides").Guide[];
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Link, useNavigate, useParams } from "@tanstack/react-router";
|
||||
import { useEffect, type MouseEvent } from "react";
|
||||
import { guides, type Guide } from "../docs/guides";
|
||||
import type { Guide } from "../docs/guides";
|
||||
import { guides } from "virtual:guides";
|
||||
import { NotFound } from "./NotFound";
|
||||
|
||||
const to = (guide: Guide) => (guide.slug ? `/docs/${guide.slug}/` : "/docs/");
|
||||
|
||||
@@ -165,7 +165,7 @@ img.shot { display: block; max-width: 100%; height: auto; }
|
||||
.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, .doc-toc { position: sticky; top: 64px; height: calc(100vh - 64px); overflow-y: auto; padding: 96px 8px 64px; margin: 0 -8px; }
|
||||
.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; }
|
||||
@@ -191,6 +191,7 @@ img.shot { display: block; max-width: 100%; height: auto; }
|
||||
.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 button + pre { padding-top: 20px; }
|
||||
.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); }
|
||||
@@ -208,7 +209,7 @@ img.shot { display: block; max-width: 100%; height: auto; }
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
.docs { grid-template-columns: minmax(0, 1fr); gap: 24px; }
|
||||
.docs-rail { position: static; height: auto; padding: 96px 0 0; }
|
||||
.docs-rail { position: static; height: auto; overflow: visible; padding: 96px 0 0; margin: 0; }
|
||||
.docs-rail ol { display: flex; flex-wrap: wrap; gap: 4px 20px; }
|
||||
.doc { padding-top: 48px; }
|
||||
.reader { grid-template-columns: minmax(0, 1fr); }
|
||||
|
||||
+28
-2
@@ -1,8 +1,34 @@
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vite";
|
||||
import { defineConfig, type Plugin } from "vite";
|
||||
import { render } from "./src/docs/guides";
|
||||
|
||||
const GUIDES = "\0virtual:guides";
|
||||
|
||||
/** The guides, rendered at build; a bad one fails the build. */
|
||||
function docs(): Plugin {
|
||||
let files: string[] = [];
|
||||
return {
|
||||
name: "ely-docs",
|
||||
resolveId: (id) => (id === "virtual:guides" ? GUIDES : undefined),
|
||||
load(id) {
|
||||
if (id !== GUIDES) return;
|
||||
const out = render();
|
||||
files = out.files;
|
||||
files.forEach((file) => this.addWatchFile(file));
|
||||
return `export const guides = ${JSON.stringify(out.guides)};`;
|
||||
},
|
||||
handleHotUpdate({ file, server }) {
|
||||
if (!files.includes(file)) return;
|
||||
const module = server.moduleGraph.getModuleById(GUIDES);
|
||||
if (module) server.moduleGraph.invalidateModule(module);
|
||||
server.ws.send({ type: "full-reload" });
|
||||
return [];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// The manifest lives beside the gallery, one folder up.
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
plugins: [react(), docs()],
|
||||
server: { fs: { allow: [".."] } },
|
||||
});
|
||||
@@ -28,6 +28,7 @@ deferred=$(grep -rn 'deferred(' src --include='*.rs' | grep -v '^src/primitives/
|
||||
instant=$(grep -rnE '(^|[^_[:alnum:]])time::(Instant|\{[^}]*Instant)' src examples || true)
|
||||
[ -z "$instant" ] || { echo "std Instant panics in the browser, use web_time::Instant:"; echo "$instant"; fail=1; }
|
||||
for f in examples/docs/*.rs; do
|
||||
[ -e "$f" ] || continue
|
||||
n=$(basename "$f" .rs)
|
||||
grep -qx "path = \"$f\"" Cargo.toml && grep -qx "name = \"docs_$n\"" Cargo.toml || { echo "$f needs [[example]] docs_$n in Cargo.toml"; fail=1; }
|
||||
done
|
||||
|
||||
Reference in new issue
Block a user