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

@plannotator/markdown-editor

v0.4.0

Published

Live-preview markdown editor for React (CodeMirror 6, atomic-editor). Raw markdown is the source of truth: rendering is decoration only, edits round-trip byte-identical.

Readme

@plannotator/markdown-editor

Live-preview markdown editor for React, built on our fork of atomic-editor (CodeMirror 6). The raw markdown text is the source of truth. Rendering is decoration only, so loading a document and reading it back is byte-identical. A one-word edit produces a one-word diff.

We pulled this out of Plannotator's edit mode, where user edits travel to AI agents as unified diffs. That workflow breaks the moment an editor normalizes markdown it didn't touch: bullet markers, escaping, spacing. This package makes the guarantee reusable and enforces it by test.

Install

pnpm add @plannotator/markdown-editor
# optional, for the default fenced-code grammar set:
pnpm add @codemirror/lang-javascript @codemirror/lang-python @codemirror/lang-json @codemirror/lang-yaml @codemirror/legacy-modes

React and the CodeMirror packages are peer dependencies. pnpm, npm 7+, and bun install them automatically. They're peers (not bundled) because your app must share one copy of React and one copy of CM6 with the editor: the codeLanguages prop takes LanguageDescription objects you build from your own @codemirror/language, and CM6 breaks when two copies of @codemirror/state coexist.

Use

import { useRef } from "react";
import { MarkdownEditor, type MarkdownEditorHandle } from "@plannotator/markdown-editor";
import "@plannotator/markdown-editor/themes/plannotator.css"; // or your own theme

function Editor({ doc }: { doc: string }) {
	const handle = useRef<MarkdownEditorHandle | null>(null);
	return (
		// Needs a height-bounded parent. The editor fills it and scrolls internally.
		<div style={{ height: "80vh" }}>
			<MarkdownEditor
				markdown={doc}
				documentId="my-doc"
				editorHandleRef={handle}
				mode="dark"
				onLinkClick={(url) => window.open(url)}
			/>
		</div>
	);
}

// Read the current text whenever you need it:
const text = handle.current?.getMarkdown();

Frozen document diff

MarkdownDiff renders two revisions as one read-only document: newer text in place, additions highlighted, deletions struck through where they used to be, and a keyboard-accessible change overview at the review pane's right edge. The full document remains visible; unchanged regions are never collapsed.

import { MarkdownDiff } from "@plannotator/markdown-editor";
import "@plannotator/markdown-editor/themes/plannotator.css";

<div style={{ height: "80vh" }}>
	<MarkdownDiff
		originalMarkdown={savedRevision}
		modifiedMarkdown={currentRevision}
		documentId={`${documentId}:${fromVersion}:${toVersion}`}
		mode="dark"
	/>
</div>;

The complete diff surface is unconstrained by default so the overview rail stays attached to the review pane edge. Set maxWidth to constrain the document and rail together, or pass showOverview={false} when the host supplies its own navigation. The extensions seam is the same one used by MarkdownEditor, including its capture-on-mount contract.

How it behaves:

  • Uncontrolled after mount. markdown is read once, then the editor owns the text. Read it back with editorHandleRef.current.getMarkdown(). Swap documents by changing documentId.

  • mode: pass your app's resolved 'dark' | 'light'. The light palette switches via data-theme="light" on the wrapper.

  • codeLanguages: defaults to a small js/ts/python/json/yaml/shell set (DEFAULT_CODE_LANGUAGES). Pass your own LanguageDescription[] to change it. Watch out: bundlers with inlineDynamicImports (single-file builds) inline every listed grammar.

  • extensions: extra CodeMirror 6 extensions, forwarded verbatim to the editor and appended after its built-ins. This is the hook for live collaboration (e.g. y-codemirror.next), custom keymaps, or update listeners — the package stays provider-agnostic and ships no collaboration code of its own. Build extensions against your own CM6 packages (one shared copy of @codemirror/state, as above). Extensions that rewrite document text void the byte-fidelity guarantee; the contract covers what the editor itself does.

    The engine ships two opt-in UI extensions (atomic-editor ≥ 0.6.0) that plug straight in:

    import { slashCommands, selectionToolbar } from "@plannotator/atomic-editor";
    
    <MarkdownEditor extensions={[slashCommands(), selectionToolbar()]} ... />

    slashCommands() is a Notion-style insert menu on / at the start of a line; selectionToolbar() is a floating bold/italic/strike/code/link bar over selected text (works multi-line and inside table cells). Both are themeable via the --atomic-editor-menu-* CSS variables and documented in the atomic-editor changelog.

  • className / cardClassName: extra classes on the wrapper and inner card, for stacking, shadows, or padding your app needs.

Theming

atomic-editor reads --atomic-editor-* CSS custom properties. Set them on .pn-markdown-editor or any ancestor. A ready-made mapping for the Plannotator token system (--foreground, --card, --primary, and so on) ships at @plannotator/markdown-editor/themes/plannotator.css. Copy it as the starting point for your own.

One gotcha that cost us a debugging session. atomic-editor declares its light palette directly on .atomic-cm-editor under [data-theme="light"], which beats variables inherited from a wrapper. Your theme must re-declare its overrides under .pn-markdown-editor[data-theme='light'] .atomic-cm-editor { ... } or they silently lose in light mode. The bundled theme does this.

The fidelity guarantee (test it in your app)

// vitest — full config below; jsdom won't work, CM6 needs layout
import { roundTrip } from "@plannotator/markdown-editor/testing";

test("my corpus round-trips byte-identically", async () => {
	for (const doc of myDocuments) {
		expect(await roundTrip(doc)).toBe(doc);
	}
});
// vitest.config.ts
export default defineConfig({
	test: {
		environment: "happy-dom",
		setupFiles: ["./setup.ts"], // must set: globalThis.IS_REACT_ACT_ENVIRONMENT = true
		server: {
			deps: {
				// Both packages must run through Vite: the editor's dist uses
				// extensionless ESM imports Node's resolver rejects, and once
				// Node loads this package natively the editor underneath is
				// loaded natively too.
				inline: ["@plannotator/markdown-editor", "@plannotator/atomic-editor"],
			},
		},
	},
});

If this fails on your content, file an issue. It's the package's core contract. Plannotator runs it against a 150-document sample of real plan history on every change.

Relationship to atomic-editor

This is a thin wrapper over @plannotator/atomic-editor, our fork of atomic-editor. We forked because we needed parser-level changes the wrapper couldn't provide (first: YAML frontmatter support, which upstream misparses as a rule plus heading). The fork tracks upstream closely, and we offer our changes back as PRs — it's a delta, not a divergence.

Development

pnpm install
pnpm lint && pnpm typecheck && pnpm test && pnpm build

Conventions: oxlint and oxfmt (tabs, width 100), strict TypeScript, vitest with happy-dom. CI runs every gate on every PR.

License

MIT