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

@lancher-dev/jaamd

v0.10.0

Published

Add markdown plugins, client-side enhancements and styles as a single Astro integration for Markdown content.

Readme


Table of Contents


Installation

npm install @lancher-dev/jaamd
# or
npx astro add @lancher-dev/jaamd

Requirements

| Requirement | Detail | |---|---| | Astro | ^7.3.0 | | Node | >=22.12.0 | | Markdown processor | Sätteri (Astro's default) or unified() | | Toolchain | Must compile TypeScript and .astro sources |

JAAMD is published as source, with no compiled dist/ and no emitted .d.ts. It works out of the box in any Astro project, but is not consumable from plain Node, a CommonJS build, or a TypeScript project using "moduleResolution": "node16".

Upgrading from 0.9

  • jaamd no longer switches the markdown processor. With no configuration you are on Sätteri.
  • Set markdown.processor: unified() to stay on unified.
  • markdown.remarkPlugins and markdown.rehypePlugins still work: Astro moves to unified, jaamd adds its remark plugins.
  • Astro ^7.3.0 is required.
  • Heading anchors are plain links: a click navigates, nothing is copied.
  • Table wrappers and heading anchors are in the built HTML. Turn them off with plugins: { tables: false, headingLinks: false }.
  • The lightbox is a <dialog class="jaamd-lightbox">. .jaamd-lightbox__backdrop, .jaamd-lightbox__content and .jaamd-lightbox--hidden are gone: style ::backdrop and [open].
  • <details> animates in CSS. .jaamd-details-wrapper and .jaamd-details-inner are gone.

Setup

Add the integration to your Astro config:

// astro.config.mjs
import { defineConfig } from "astro/config";
import jaamd from "@lancher-dev/jaamd";

export default defineConfig({
  integrations: [jaamd()],
});

Wrap your markdown content with the MarkdownContent component in your layout:

---
// src/layouts/BlogPost.astro
import MarkdownContent from "@lancher-dev/jaamd/components";
---
<MarkdownContent>
  <slot />
</MarkdownContent>

The integration registers its plugins on the configured markdown processor and injects the stylesheet automatically. No other configuration is required.

The processor is extended, never replaced: Sätteri gets mdast plugins and the directive syntax, unified() gets the remark plugins. Any other processor is left alone, with a warning.

[!IMPORTANT] <MarkdownContent> loads the stylesheets for you. If you render markdown without it, import them yourself in your layout frontmatter:

---
import "@lancher-dev/jaamd/default.css";
import "@lancher-dev/jaamd/styles.css";
---

Integration Options

jaamd({
  selector:   ".jaamd-content", // CSS selector for the JS enhancements
  theme:      "github-light",   // Shiki theme name (or { light, dark })
  noDefault:  false,            // skip injecting @lancher-dev/jaamd/default variable fallbacks
  plugins: {
    codeTabs:  true,            // :::code-tabs directive blocks
    toc:       true,            // :::toc directive blocks
    alerts:    true,            // > [!NOTE] / [!WARNING] blockquote alerts
    directive: true,            // `:::` syntax (prerequisite for codeTabs and toc)
    tables:    true,            // scroll container around tables
    headingLinks: true,         // anchor link on every heading
  },
})

selector

Controls which element the client-side JS enhancements target at runtime. It does not affect the CSS file, which always uses .jaamd-content.

  • With <MarkdownContent>, leave it at the default.
  • With a custom wrapper (e.g. <div data-md>), set selector to match it and provide your own CSS.

noDefault

Set to true when you supply a complete --jaamd-* variable set of your own. It has no effect on <MarkdownContent>, which imports the defaults directly; with a custom wrapper, control them by importing @lancher-dev/jaamd/default or not.

Markdown Syntax

Alerts

GitHub-style blockquote alerts, in five variants:

> [!NOTE]
> Useful information the reader should know.

> [!TIP]
> Helpful advice.

> [!IMPORTANT]
> Key information required to succeed.

> [!WARNING]
> Urgent information needing immediate attention.

> [!CAUTION]
> Advises about risks or negative outcomes.

Code tabs

Group several code blocks into a tabbed panel. The text after the language is used as the tab label; it falls back to the language, then to Tab N.

:::code-tabs
```bash npm
npm install
```
```bash pnpm
pnpm install
```
:::

Table of contents

A :::toc block wraps the list you write in a navigation landmark. Nothing is generated: entries, order, wording and depth are whatever the markdown says. The text in brackets is an optional title.

:::toc[On this page]
- [Installation](#installation)
- [Setup](#setup)
  - [Integration options](#integration-options)
:::

It renders as:

<nav class="jaamd-toc" aria-label="On this page">
  <p class="jaamd-toc-title">On this page</p>
  <ul>…your list…</ul>
</nav>

The title is a <p>: out of getHeadings(), no anchor icon. A - list loses its bullets, 1. keeps the numbers. Entries are plain anchors and navigate with JavaScript disabled; with it, the entry whose section is on screen is marked aria-current="location".

Writing the anchors

Heading ids come from Astro, slugged with github-slugger:

| Heading | Anchor | |---|---| | ## Getting started | #getting-started | | ## Details / accordion | #details--accordion (one dash per space, the / dropped between them) | | ## What's new? | #whats-new (punctuation removed, not replaced) | | ## snake_case | #snake_case (underscores survive) | | a second ## Setup | #setup-1 |

The client-side fallback follows the same rules, so ids match for markdown that never went through Astro's pipeline (set:html, HTML from a CMS).

With a sticky header, raise --jaamd-scroll-margin-top (default 80px).

[!NOTE] :::toc needs the directive syntax, on by default. With plugins: { toc: false } the block degrades to a plain <div>: list and links intact, unstyled.

Spoilers

Any element with the spoiler class is hidden until activated:

<span class="spoiler">The butler did it.</span>

MarkdownContent Component

MarkdownContent is a polymorphic component. It renders as <div> by default and accepts any valid HTML tag via the as prop.

import MarkdownContent from "@lancher-dev/jaamd/components";

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | as | HTMLTag | "div" | The HTML element to render as. | | class | string | - | Extra CSS classes appended to the wrapper. | | ...rest | - | - | All standard HTML attributes for the chosen as element (e.g. id, data-*, aria-*). |

The jaamd-content class is always present on the wrapper element. It is the selector used by the JS enhancements and must not be removed.

---
import MarkdownContent from "@lancher-dev/jaamd/components";
---

<!-- Default: renders as <div class="jaamd-content"> -->
<MarkdownContent>
  <slot />
</MarkdownContent>

<!-- Custom tag: renders as <article class="jaamd-content"> -->
<MarkdownContent as="article">
  <slot />
</MarkdownContent>

<!-- Extra classes: renders as <article class="jaamd-content prose mx-auto"> -->
<MarkdownContent as="article" class="prose mx-auto">
  <slot />
</MarkdownContent>

Client-side Enhancements

A dependency-free ES module enhances the rendered markdown. Events are delegated on document; per-element setup re-runs on every astro:page-load, so it keeps working across View Transitions.

Table wrappers and heading anchors come from the build. The module adds them only to markup that skipped the processor (set:html, HTML from a CMS).

| Enhancement | Behaviour | |---|---| | Heading links | Adds an anchor link to h1–h6 where the build did not. Fills in a missing id with a github-slugger-compatible slug, de-duplicated across the page. | | Table of contents | Marks the :::toc entry whose section is on screen with aria-current="location". | | Copy buttons | Adds a copy button to every pre. Without clipboard access it selects the code instead. The only clipboard write in the package. | | Image lightbox | Click an image to open it in a modal <dialog>. Closes on a click outside the image, the ✕ button or Esc. | | Responsive tables | Wraps a table in a horizontally scrollable container where the build did not. | | Code tabs | Drives the :::code-tabs tablist. | | Spoilers | Reveals .spoiler content on click or Enter/Space. |

Keyboard

| Element | Keys | |---|---| | Code tabs | ← → move between tabs, Home End jump to first/last. Panels are focusable so wide samples can be scrolled. | | Spoilers | Tab to focus, Enter or Space to reveal. Hover does not reveal. | | Lightbox | Esc to close. |

<details> animates in CSS, no script: height where the browser supports interpolate-size, a fade elsewhere.

Under prefers-reduced-motion: reduce JAAMD's CSS transitions are disabled.

With scripting disabled, tables still scroll, heading anchors work and every code tab panel is shown.

Excluding images from the lightbox

Linked images are skipped automatically, so badges keep navigating:

[![build](https://img.shields.io/badge/build-passing-green)](https://github.com/you/repo)

Opt a standalone image out with data-no-lightbox:

<img src="/diagram.svg" alt="Architecture diagram" data-no-lightbox />

Theming

All styles are driven by CSS custom properties prefixed with --jaamd-*. The default set (@lancher-dev/jaamd/default) is injected automatically.

Customizing variables

Override any variable on :root in your own stylesheet:

:root {
  --jaamd-color-fg:            #334155;
  --jaamd-color-fg-bright:     #0f172a;
  --jaamd-color-primary:       #6366f1;
  --jaamd-color-primary-light: #818cf8;
  --jaamd-font-mono: ui-monospace, monospace;
  --jaamd-font-size: 1rem;
}

Defaults live in @layer jaamd.defaults, so an unlayered :root block always wins regardless of import order.

See src/styles/variables.css for all 60 variables and their default values.

Font sizes

--jaamd-font-size is the one knob for the whole document. Every other size is derived from it, so moving it moves the entire scale in proportion:

:root {
  --jaamd-font-size: 1rem; /* headings, code, tables… all follow */
}

To break a single element out of that scale, set its own token. Each is read with the derived value as its fallback, so an unset token changes nothing; they are opt-in, one at a time:

:root {
  --jaamd-font-size: 1rem; /* the baseline */
  --jaamd-font-size-h1: 2.5rem; /* but h1 is fixed, not proportional */
  --jaamd-font-size-code-block: 0.8rem; /* and code blocks are tighter */
}

| Token | Applies to | Default when unset | |---|---|---| | --jaamd-font-size-h1 … -h4 | h1–h4 | 2.22 / 1.67 / 1.33 / 1.11 × base | | --jaamd-font-size-h5, -h6 | h5, h6 | base | | --jaamd-font-size-p, -li | paragraphs, list items | base | | --jaamd-font-size-blockquote | blockquote, contents included | base | | --jaamd-font-size-code | inline `code` | 0.78 × base | | --jaamd-font-size-code-block | fenced code blocks | 0.85 × base | | --jaamd-font-size-table | th and td together | 0.78 × base | | --jaamd-font-size-summary | <details> summary | 0.89 × base | | --jaamd-font-size-alert-title | alert titles | 0.83 × base | | --jaamd-font-size-toc | :::toc block, contents included | 0.94 × base | | --jaamd-font-size-toc-title | :::toc title | 0.83 × base | | --jaamd-font-size-footnotes | footnotes block, contents included | 0.85 × base |

A block that owns a token governs what is inside it: paragraphs in a blockquote follow -blockquote, not -p. Alert bodies have no token of their own and follow -p; only their title is separate. The copy button, code-tab labels and heading-link icons stay tied to --jaamd-font-size: they are JAAMD's chrome, not your document's typography.

Dark mode

The default set includes dark-mode overrides activated by the dark class on <html>. Toggle the class and all JAAMD elements adapt.

Theme presets

Eight presets restyle the --jaamd-* palette to match popular editor colour schemes. A dual preset carries a light and a dark palette and follows html.dark; the others are one palette that applies in both modes.

| Preset | Import | Kind | Recommended Shiki theme | |--------|--------|------|------------------------| | Catppuccin | @lancher-dev/jaamd/themes/catppuccin | dual | catppuccin-latte / catppuccin-mocha | | Dracula | @lancher-dev/jaamd/themes/dracula | dark | dracula | | Gruvbox | @lancher-dev/jaamd/themes/gruvbox | dual | gruvbox-light-medium / gruvbox-dark-medium | | Nord | @lancher-dev/jaamd/themes/nord | dark | nord | | One Dark | @lancher-dev/jaamd/themes/one-dark | dark | one-dark-pro | | Rosé Pine | @lancher-dev/jaamd/themes/rose-pine | dual | rose-pine-dawn / rose-pine | | Rosé Pine Moon | @lancher-dev/jaamd/themes/rose-pine-moon | dual | rose-pine-dawn / rose-pine-moon | | Tokyo Night | @lancher-dev/jaamd/themes/tokyo-night | dark | tokyo-night |

As a standalone theme, replacing the default light theme:

jaamd({ theme: "dracula" })
@import "@lancher-dev/jaamd/default.css";
@import "@lancher-dev/jaamd/themes/dracula.css";
@import "@lancher-dev/jaamd/styles.css";

A single-palette preset can also be scoped to html.dark via the /dark variant, to use it as the dark half of your own light theme. Dual presets already cover both modes and have no /dark:

@import "@lancher-dev/jaamd/themes/dracula/dark.css";
import "@lancher-dev/jaamd/themes/dracula/dark";

[!IMPORTANT] A preset declares a palette, not the whole variable set: surfaces, borders and the table-of-contents card are derived from it by default.css. Load the defaults alongside it. Do not pair a preset with noDefault: true: those derived tokens fall back to the light palette.

Each theme declares itself, so a picker can be built from data:

import { themes } from "@lancher-dev/jaamd/themes";
// [{ slug: "dracula", name: "Dracula", mode: "dark", shiki: "dracula" }, …]

Each preset is generated from a palette.js of fourteen colours, so every theme declares the same token set. The .css files are build output: to customise a preset, copy its directory and edit the palette. To add one, see CONTRIBUTING.md.

Dual-theme Shiki

Pass an object to theme to highlight code with two colour schemes, switched by the dark class:

jaamd({
  theme: { light: "github-light", dark: "github-dark" },
})

Add an inline script in <head> to prevent a flash of the wrong theme:

<script is:inline>
  (function () {
    var t = localStorage.getItem("theme");
    if (t === "dark" || (!t && matchMedia("(prefers-color-scheme: dark)").matches))
      document.documentElement.classList.add("dark");
  })();
</script>

Manual / Advanced Usage

To bypass the integration, register the plugins yourself on the processor you use. On Sätteri, Astro's default:

// astro.config.mjs
import { satteri } from "@astrojs/markdown-satteri";
import {
  satteriAlert,
  satteriCodeTabs,
  satteriToc,
  satteriDirectiveFallback,
  satteriTableWrapper,
  satteriHeadingLinks,
} from "@lancher-dev/jaamd";

export default defineConfig({
  markdown: {
    processor: satteri({
      features: { directive: true },
      mdastPlugins: [satteriAlert, satteriCodeTabs, satteriToc, satteriDirectiveFallback],
      hastPlugins: [satteriTableWrapper, satteriHeadingLinks],
    }),
  },
});

satteriDirectiveFallback keeps directives no plugin handles, which Sätteri drops: blocks as a plain <div>, an inline :name as typed. Put it last.

On unified:

// astro.config.mjs
import { unified } from "@astrojs/markdown-remark";
import {
  remarkAlert,
  remarkCodeTabs,
  remarkDirective,
  remarkToc,
  rehypeTableWrapper,
  rehypeHeadingLinks,
} from "@lancher-dev/jaamd";

export default defineConfig({
  markdown: {
    processor: unified({
      remarkPlugins: [remarkAlert, remarkDirective, remarkCodeTabs, remarkToc],
      rehypePlugins: [rehypeTableWrapper, rehypeHeadingLinks],
    }),
  },
});
---
import "@lancher-dev/jaamd/default";  // variable fallbacks; omit if you provide your own
import "@lancher-dev/jaamd/styles";
---
<div class="jaamd-content">
  <slot />
</div>
<script>
  import { initMarkdownEnhancements } from "@lancher-dev/jaamd/client";
  function run() { initMarkdownEnhancements(".jaamd-content"); }
  run();
  document.addEventListener("astro:page-load", run);
</script>

The CSS files can also be imported from plain .css:

@import "@lancher-dev/jaamd/default.css";
@import "@lancher-dev/jaamd/styles.css";