@docspack/sheaf-react
v0.1.0
Published
The Sheaf documentation shell — sidebar, page, table of contents and pager, composed from cascivo and mountable inside an app you already have.
Downloads
271
Maintainers
Readme
@docspack/sheaf-react
The Sheaf documentation shell — sidebar, article, table of contents and pager, composed from cascivo. It mounts inside an app you already have rather than generating one you don't.
import { buildGraph } from "@docspack/sheaf";
import { DocsPage, findPage } from "@docspack/sheaf-react";
import "@cascivo/react/styles.css";
import "@cascivo/themes/base.css";
const graph = await buildGraph("./docs");
const page = findPage(graph, "getting-started");
<DocsPage graph={graph} page={page} html={renderedHtml} basePath="/docs" />;It takes rendered HTML
The shell does not render Markdown. Your host already has a pipeline — Astro's, a Vite plugin's — and a second one inside a React package would ship a parser to the browser and let two renderers disagree about one document.
What matters is that the heading ids in that HTML match the graph's heading slugs, so the
table of contents links somewhere. @docspack/sheaf proves its slugs match github-slugger
and Astro's processor; this package proves the anchors and ids land in the same document.
Components
| | |
| --- | --- |
| DocsPage | All four together — the drop-in. |
| DocsSidebar | Every page in reading order, current one marked. |
| DocsArticle | Title, summary, prose. |
| DocsToc | On this page. Renders nothing when there are no sections. |
| DocsPager | Previous and next, in the graph's order. |
Plus the pure helpers the components use: toNavItems, toTocEntries, neighbours,
findPage, pageHref. None of them import React.
An API reference
The same shell renders an OpenAPI document, from the model @docspack/openapi produces:
import { parseOpenApi } from "@docspack/openapi";
import { ApiReference, ApiSidebar } from "@docspack/sheaf-react";
const document = parseOpenApi(JSON.parse(await readFile("openapi.json", "utf8")));
<ApiSidebar document={document} basePath="#" label="Endpoints" />
<ApiReference document={document} />;| | |
| --- | --- |
| ApiReference | Every operation, grouped by tag — the drop-in. |
| ApiOperation | One operation: auth, parameters, schemas, responses, samples, console. |
| ApiSidebar | Operations by tag, each with its method. |
| ApiSchemaTree | A shape as a nested field list, expanding named types inline. |
| ApiSamples | cURL, JavaScript, Python and Go. |
| ApiConsole | The try-it form. |
Two controls need behaviour, and neither has any until you add one line:
import { mountApiConsoles, mountApiSamples } from "@docspack/openapi/console";
mountApiSamples(); // makes the language tabs switch, and remembers the choice
mountApiConsoles(); // enables each Send buttonBoth default to the state a reader gets if that script never runs: every sample visible and labelled rather than a tab strip with nothing behind it, and a Send button that is visibly disabled rather than inert. The console reads its credential at send time and stores it nowhere.
The sample tabs need two things from the host
The tab strip and all four panels are rendered, not built in the browser — building them meant
painting every sample and then collapsing them, which on a fourteen-operation reference was an
11,000px reflow. Which of the two states a reader sees is decided by one attribute on the document
element, data-api-lang, and that makes it your stylesheet's decision rather than a script's.
Set it before the first paint, from an inline script ahead of the reference:
<script>
try {
const stored = localStorage.getItem("docspack-api-language");
document.documentElement.setAttribute("data-api-lang", stored ?? "curl");
} catch {
document.documentElement.setAttribute("data-api-lang", "curl");
}
</script>Then one rule per language, because CSS cannot test one attribute against another:
/* No attribute means no script: show every sample, hide the strip that cannot switch. */
.api-tabs { display: none; }
:root[data-api-lang] .api-tabs { display: flex; }
:root[data-api-lang] [data-api-sample-label] { display: none; }
:root[data-api-lang="curl"] .api-sample:not([data-api-language="curl"]),
:root[data-api-lang="javascript"] .api-sample:not([data-api-language="javascript"]),
:root[data-api-lang="python"] .api-sample:not([data-api-language="python"]),
:root[data-api-lang="go"] .api-sample:not([data-api-language="go"]) {
display: none;
}SAMPLE_LANGUAGE_CHOICE and SAMPLE_LANGUAGE_KEY are exported from @docspack/openapi so the
attribute and the storage key are not typed twice. A language with no rule renders a tab that hides
every panel, so it is worth a test that walks SAMPLE_LANGUAGES against your stylesheet.
The choice is one attribute in one place, so a reader who picks Python has picked it for every operation on the page and for the next page too.
An operation's description is Markdown; pass html to render it with your own pipeline, as
with DocsArticle.
Styling it
The components emit semantic class names — .api-operation, .api-field, .api-samples,
.api-console and the rest — and no CSS. Code blocks use .code-block / .code-head /
.code-lang wrapping a <pre tabindex="0">, which is the markup a documentation site's Markdown
pipeline usually already emits: style it once and both halves match, and a copy-button script
written for one finds the other. apps/web/src/styles/global.css in this repository is a worked
example.
No client JavaScript
Every component is a pure function of props and holds no state, so the shell renders to static HTML. In Astro that means no client directive — which is also the only configuration where cascivo's per-component CSS reliably arrives.
Routing
Pass basePath. For router links in the sidebar, call cascivo's setLinkComponent once in
your app; the shell imports no router itself.
Peers
react, react-dom, @cascivo/react, @preact/signals-react.
MIT © docspack
