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:
1 parent
157176e2fb
commit
fba90c22e6
12 files changed
+305
-6
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. 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 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).
|
||||
|
||||
## Commands
|
||||
|
||||
|
||||
@@ -87,3 +87,7 @@ codegen-units = 1
|
||||
[[example]]
|
||||
name = "gallery"
|
||||
path = "examples/gallery/main.rs"
|
||||
|
||||
[[example]]
|
||||
name = "docs_hello"
|
||||
path = "examples/docs/hello.rs"
|
||||
@@ -162,8 +162,22 @@ Per-component lines live in `tasks/`. Tags there:
|
||||
- [x] T45e Site · Local: the gallery bundle built for localhost and the site served for the owner to see; not published
|
||||
- [x] T46 E2E: Playwright against the built site
|
||||
- [x] T46a Site · Owner's notes: the city title whole, header icons on one line, less space above a story, no visible scroll bars, each story named by its first component, every page laid out at phone, tablet and desktop widths
|
||||
- [ ] T47 Ship: GitHub repo (public), CI, Cloudflare deploy through `npx wrangler`
|
||||
- [ ] T48 Acceptance: live URL and MVP checklist
|
||||
- [x] T47 Ship: GitHub repo (public), CI, Cloudflare deploy through `npx wrangler`
|
||||
- [ ] T48 Docs: a Docs tab after gpui-kit.com/docs, a rail of guides, an on-this-page list, previous and next; every claim checked against the code, every full Rust sample an example `scripts/check.sh` compiles and the page imports as text
|
||||
- [ ] T48a Docs · Frame: the Docs tab, `/docs/` and `/docs/$page/`, Markdown pages, the rail, the on-this-page list, previous and next, code blocks, light and dark, phone to desktop
|
||||
- [ ] T48b Docs · Introduction: what Ely is, the order to read, what is stable, a first example, license
|
||||
- [ ] T48c Docs · Installation: Rust, the git dependencies, gpui_platform's fonts, runtime shaders, the gallery as a check, what `init` refuses
|
||||
- [ ] T48d Docs · Getting started: an app from `init` to a window under a root `FocusScope`, with a Button
|
||||
- [ ] T48e Docs · Theme: modes, tokens through `cx.theme()`, high contrast, color-blind safe charts, an owner's palette
|
||||
- [ ] T48f Docs · Focus and keyboard: Tab and Shift-Tab, the root scope, tab stops, the focus ring, overlays that take and give back focus
|
||||
- [ ] T48g Docs · Motion: durations, reduced motion, springs, motion from a change
|
||||
- [ ] T48h Docs · Icons, fonts and assets: `Assets`, `IconName`, adding icons, the embedded fonts, file icons
|
||||
- [ ] T48i Docs · Overlays: what draws over the page, dialogs, popovers, toasts
|
||||
- [ ] T48j Docs · Internationalization: the `I18n` global, catalogs, right-to-left
|
||||
- [ ] T48k Docs · WebAssembly: the web gallery build, its URL, the host protocol, what stays native
|
||||
- [ ] T48l Docs · Testing: the `test-support` feature, frames, clocks and reduced motion in tests
|
||||
- [ ] T48m Docs · E2E: the Docs routes, rail, list, previous and next, and code blocks under Playwright
|
||||
- [ ] T49 Acceptance: live URL and MVP checklist
|
||||
|
||||
## T00 detail
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
use ely_gpui_component::{Assets, buttons::Button, primitives::FocusScope, theme::ActiveTheme};
|
||||
use gpui::{
|
||||
App, AppContext, Context, FocusHandle, IntoElement, ParentElement, Render, Styled, Window,
|
||||
WindowOptions,
|
||||
};
|
||||
|
||||
struct Hello {
|
||||
focus: FocusHandle,
|
||||
presses: usize,
|
||||
}
|
||||
|
||||
impl Render for Hello {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let colors = &cx.theme().colors;
|
||||
let this = cx.entity();
|
||||
FocusScope::new(&self.focus)
|
||||
.root()
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_4()
|
||||
.bg(colors.bg)
|
||||
.text_color(colors.fg)
|
||||
.child(format!("Pressed {} times", self.presses))
|
||||
.child(
|
||||
Button::new("press", "Press")
|
||||
.primary()
|
||||
.on_click(move |_, _, cx| {
|
||||
this.update(cx, |hello, cx| {
|
||||
hello.presses += 1;
|
||||
cx.notify();
|
||||
});
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
gpui_platform::application()
|
||||
.with_assets(Assets)
|
||||
.run(|cx: &mut App| {
|
||||
ely_gpui_component::init(cx);
|
||||
cx.open_window(WindowOptions::default(), |window, cx| {
|
||||
cx.new(|cx| {
|
||||
let focus = cx.focus_handle();
|
||||
window.focus(&focus, cx);
|
||||
Hello { focus, presses: 0 }
|
||||
})
|
||||
})
|
||||
.expect("window failed to open");
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Generated
+10
@@ -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
|
||||
|
||||
@@ -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")}>
|
||||
|
||||
@@ -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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
||||
|
||||
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);
|
||||
@@ -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.
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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
@@ -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; }
|
||||
}
|
||||
Reference in new issue
Block a user