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

@markup-carve/carve-css

v0.1.0

Published

Stylesheet for the HTML the Carve markup language renders - admonitions, tab sets, code groups, callouts, figures, footnotes, glossary and index

Readme

carve-css

Stylesheet for the HTML the Carve markup language renders.

npm install @markup-carve/carve-css
@import "@markup-carve/carve-css";

Then put the class on whatever element holds rendered Carve:

<article class="carve">
  <!-- carveToHtml output -->
</article>

Everything is scoped under .carve, so this cannot reach a host page's own markup — which matters when the host is a WordPress admin screen or a Shopware storefront rather than a documentation site.

Why this exists

Carve's rendered HTML is pinned by the spec: the class on an admonition, a tab set, a code group, a figure or a callout is the same out of every engine. Six repositories in this organization were nonetheless each writing that CSS by hand — carve-press, wp-carve, hugo-carve, carve-pdf, zensical-carve-demo and shopware-carve.

The cost was not the duplication. It was that each copy covered a different subset:

| Construct | press | zensical | pdf | wp | hugo | | --- | --- | --- | --- | --- | --- | | admonitions | yes | — | yes | yes | yes | | tab sets | — | yes | yes | yes | — | | code groups | yes | — | yes | yes | yes | | spoilers | — | yes | yes | yes | yes | | table of contents | — | yes | yes | yes | — | | code callouts | — | yes | — | — | — | | glossary | — | yes | — | — | — | | index | — | yes | — | — | — | | critic markup | — | — | — | — | yes |

So the union of six hand-written stylesheets still left callouts, the glossary, the index and critic markup unstyled everywhere but one repo each. A construct lands in the language, and six themes have to notice separately.

Layers

| File | What it covers | | --- | --- | | tokens.css | every colour, space and font, as custom properties | | core.css | what the core renderer emits, with no extensions | | extensions.css | what the bundled extensions add | | recipes.css | conventions the engine does not know: trees, cards, columns, badges | | print.css | paper: page breaks, printed URLs, open disclosures | | carve.css | tokens, core and extensions, in dependency order |

Take the layers you need:

@import "@markup-carve/carve-css/tokens.css";
@import "@markup-carve/carve-css/core.css";
/* skip extensions.css if you render without extensions */

print.css is deliberately not in the bundle, because whether it applies always or only when printing is yours to decide. Inside a media query for the ordinary case:

<link rel="stylesheet" href="…/print.css" media="print">

Unconditionally when a headless browser is the printer, which is how carve-pdf works and why it needed its own print sheet before this existed.

Recipes

::: name is Tier-1 core syntax that is always on, and a word with no registered handler falls through to a generic <div class="name">. So this

::: tree
- src/
  - parser/
    - blocks.crv
- tests/
:::

already renders as <div class="tree"><ul>… out of every engine - no extension, no configuration, no parser change. The only thing missing is CSS.

recipes.css supplies it, for trees, card decks, columns, galleries, numbered steps, margin notes, scroll and full-width containers, lead paragraphs, status badges, and table modifiers including per-row status:

@import "@markup-carve/carve-css";
@import "@markup-carve/carve-css/recipes.css";

It is not in the bundle, and unlike print.css that is not about when the rules apply - it is about what they are. Everything in core.css and extensions.css styles HTML the spec pins; these class names are a convention this package proposes. Opting in is how you say you use the words the way carve-css means them. It also keeps columns, cards, scroll and wide - generic enough that a host's own framework may define them - out of anyone's page who did not ask for them.

Recipes read the same tokens as the rest of the package, plus a few of their own with fallbacks, so an instance can be retuned without overriding a selector:

| Property | Default | Used by | | --- | --- | --- | | --carve-tree-guide | --carve-border | the tree's connector lines | | --carve-tree-indent | 0.95em | one level of tree nesting | | --carve-gallery-ratio | 4 / 3 | gallery tiles | | --carve-step-marker | 1.5rem | the numbered circle on a step; the text gutter follows it | | --carve-wide-size | 100% | how far ::: wide may spread | | --carve-aside-size | 14rem | a floated margin note |

Several take a data-* attribute from the source instead of a second class - {.tree data-guides="dotted"}, {data-columns="3"}, [beta]{.badge data-tone="warn"} - so one class covers every variant rather than multiplying into .columns-2, .columns-3 and whatever comes next.

Theming

Override tokens, not selectors. That is the whole interface:

:root {
  --carve-accent: #7c3aed;
  --carve-font-mono: "Berkeley Mono", monospace;
  --carve-radius: 0;
}

Every rule in the package resolves through these, so an override reaches the construct without you needing to know which selector styles it.

Admonitions take a second level: each type maps to a semantic pair, and the pair is itself a token, so recolouring one kind is two lines.

.carve .admonition.deprecated {
  --carve-adm: var(--carve-danger);
  --carve-adm-wash: var(--carve-danger-wash);
}

The admonition type comes from the source (::: whatever), so the vocabulary is open. The base .admonition rule stands on its own for a type this package has never heard of; note, info, tip, success, hint, warning, caution, attention, danger, error, bug and important get colours.

Fonts and themes

No @font-face and no @import of a font host. A stylesheet that reaches out for a font cannot be used behind a strict CSP, and every consumer here already has a type stack — so --carve-font-body and --carve-font-heading inherit by default.

Dark mode covers all three theme states: :root carries the light palette, a prefers-color-scheme block handles an unstamped dark root, and [data-theme="dark"] handles an explicit toggle. Washes go dark rather than inverting, because a pale wash on a dark ground is a light box the reader's eye has to fight.

If your host has its own theme toggle, map it to data-theme

Plenty of hosts signal dark with a class instead - VitePress and Tailwind both use dark on the root element. This package reads data-theme, so a class alone reaches nothing and the palette silently stays light: white cards on a dark page.

Map it in both directions, not just dark. A host that signals light by stamping nothing leaves the prefers-color-scheme block matching, so on a dark-OS machine a light page picks up the dark tokens - the same bug pointing the other way, and the one people miss because they only test the toggle they were fixing.

const root = document.documentElement
const sync = () =>
  root.setAttribute('data-theme', root.classList.contains('dark') ? 'dark' : 'light')

sync()
new MutationObserver(sync).observe(root, { attributes: true, attributeFilter: ['class'] })

Run it before first paint - from a <head> script rather than after hydration - or the first frame shows the wrong palette. A host that renders its own shell, like an IDE preview panel, can simply write the attribute when it builds the document.

Two details a hand-written theme usually misses

Both were found by reading real engine output rather than the syntax guide:

  • The footnote section carries no class. It is <section role="doc-endnotes">, with [role="doc-noteref"] on the reference and [role="doc-backlink"] on the return arrow. A theme selecting .footnotes styles nothing.
  • A quote with an attribution is a <figure>, not a <blockquote> — the quote is wrapped and the attribution is its <figcaption>. A rule targeting blockquote cite never fires.

There is also a real cross-engine difference worth knowing: carve-js renders a tab set as radio inputs (.tabs-radio / .tabs-label / .tabs-panel, working with no JavaScript), while carve-rs renders .tabs > .tab children with no interaction. Both shapes are styled here — the second as stacked labelled sections rather than as tabs pretending to be clickable.

The coverage gate

npm test

Renders test/constructs.crv through @markup-carve/carve with every extension on, extracts every class and ARIA role from the output, and fails when one has neither a rule nor a named exemption in scripts/check-coverage.mjs.

This is the point of the package. The failure it prevents is the one that happened six times: a construct arrives, the stylesheet written from the syntax guide has no rule for it, nothing goes red, and the construct renders unstyled until someone files it against the integration instead of the theme.

The gate is verified to actually fail — removing a class's only rule turns it red, and a set of self-assertions on its matcher runs first, because the loose version of that matcher shipped before the strict one and made the whole check hollow (.callout was satisfied by a .callouts rule).

When the language grows a construct, add it to test/constructs.crv. A construct missing from the fixture is one the gate cannot see.