npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

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 button

Both 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