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

@atomic-editor/editor

v0.3.0

Published

CodeMirror 6 markdown editor with Obsidian-style inline live preview.

Readme

Atomic Editor

A CodeMirror 6 markdown editor with Obsidian-style inline live preview. Renders headings, bold, italic, links, images, and tables WYSIWYG while keeping the raw markdown as the source of truth — so copy, save, and interop with any other markdown tool Just Works.

Originally built for Atomic, a personal knowledge base, now standalone.

  • Virtualized: CM6 renders only the viewport. Open a 500-page atom instantly; scrolling stays smooth even on iOS.
  • Layout-stable: no reflow when you click into a heading or move the cursor. Inline decorations rather than block-level widget swaps.
  • WYSIWYG tables: click into a cell to edit in place; wide tables scroll horizontally inside a contained wrapper.
  • Smart lists: Enter continues tight bullets and task checkboxes, Enter on an empty item dedents, - [ ] becomes a real checkbox.
  • Fenced code highlighting for 20+ languages, lazy-loaded per fence so unused grammars never hit the wire.
  • Theme with CSS variables — dark by default, light via a single data-theme="light" attribute.
  • Minimal search panel (Ctrl/Cmd+F) styled to match the editor.

Live demo →

Install

npm install @atomic-editor/editor \
  @codemirror/state @codemirror/view @codemirror/commands \
  @codemirror/autocomplete @codemirror/language @codemirror/search \
  @codemirror/lang-markdown \
  @lezer/common @lezer/highlight \
  react react-dom

The CodeMirror and React packages are declared as peer dependencies rather than regular deps. You install them alongside the editor so your bundler resolves a single shared copy — two copies of @codemirror/state in one bundle would silently break the editor's state-field identity checks.

Fenced-code language grammars (@codemirror/lang-javascript, @codemirror/lang-python, etc.) are optional peers — install only the ones you want highlighted. See Syntax highlighting below.

Use

import { AtomicCodeMirrorEditor } from '@atomic-editor/editor';
import '@atomic-editor/editor/styles.css';

function App() {
  return (
    <AtomicCodeMirrorEditor
      markdownSource={'# Hello\n\nA paragraph.'}
      onMarkdownChange={(md) => console.log(md)}
      onLinkClick={(url) => window.open(url, '_blank', 'noopener,noreferrer')}
    />
  );
}

The editor fills its parent — wrap it in a height-bounded flex or grid container.

Imperative handle

Pass a ref if you need to drive the editor from outside — e.g. wire your own toolbar buttons, or open the search panel from a global keybinding:

import { useRef } from 'react';
import {
  AtomicCodeMirrorEditor,
  type AtomicCodeMirrorEditorHandle,
} from '@atomic-editor/editor';

function App() {
  const editor = useRef<AtomicCodeMirrorEditorHandle | null>(null);
  return (
    <>
      <button onClick={() => editor.current?.openSearch()}>Search</button>
      <AtomicCodeMirrorEditor
        markdownSource={'…'}
        editorHandleRef={editor}
      />
    </>
  );
}

Methods: focus, undo, redo, openSearch(query?), closeSearch, revealText(query), isSearchOpen, getMarkdown, getContentDOM.

Arriving from a search result

Two props drop the user near a relevant paragraph on mount:

  • initialSearchText opens the search panel pre-filled with the query. Full navigation surface — arrow keys to step through matches, close to dismiss. Good when the user explicitly invoked find.
  • initialRevealText does a less intrusive scroll-into-view with a 3.2 s fade-out highlight on the first match — no panel, no cursor move. Good for "I clicked a search result, take me to the paragraph it came from".

Both accept string | null. The reveal matcher falls back progressively — exact, whitespace-collapsed, individual lines, then truncated prefixes (140 and 80 chars) — so hits still resolve when the query came from an LLM-massaged snippet that doesn't match the source byte-for-byte. For post-mount reveals, call editorHandle.revealText(query) via the imperative handle.

The fade highlight uses CSS variables --atomic-editor-initial-reveal-bg and --atomic-editor-initial-reveal-bg-strong; override to theme the peak and settled colors independently of the main search-match palette.

Syntax highlighting

Fenced code blocks are plain monospace by default. To enable highlighting, pass a codeLanguages array. @codemirror/lang-markdown dynamically imports each grammar the first time a fence uses it, so large lists don't bloat the initial bundle.

Option 1: use the curated list (~20 languages)

# Install the lang-* peers you want highlighted.
npm install \
  @codemirror/lang-javascript @codemirror/lang-python \
  @codemirror/lang-rust @codemirror/lang-go @codemirror/lang-html \
  @codemirror/lang-css @codemirror/lang-json @codemirror/lang-yaml \
  @codemirror/legacy-modes  # ruby/swift/shell/toml/dockerfile
import { AtomicCodeMirrorEditor } from '@atomic-editor/editor';
import { ATOMIC_CODE_LANGUAGES } from '@atomic-editor/editor/code-languages';

<AtomicCodeMirrorEditor
  markdownSource={'…'}
  codeLanguages={ATOMIC_CODE_LANGUAGES}
/>

See src/code-languages.ts for the full list (JavaScript, TypeScript, Python, Go, Rust, Ruby, Java, C, C++, PHP, Swift, Shell, SQL, HTML, CSS, XML, JSON, YAML, TOML, Dockerfile, Markdown).

Option 2: bring your own

import { LanguageDescription } from '@codemirror/language';
import { python } from '@codemirror/lang-python';

const codeLanguages = [
  LanguageDescription.of({
    name: 'Python',
    alias: ['py'],
    extensions: ['py'],
    load: () => Promise.resolve(python()),
  }),
];

<AtomicCodeMirrorEditor markdownSource={'…'} codeLanguages={codeLanguages} />

Theming

Every color, font, and size reads from a CSS custom property with an inline fallback. Override on any ancestor of the editor.

The package ships a light variant that activates whenever data-theme="light" is set on an ancestor — including <html> or <body>. The dark defaults remain unchanged; the light block just re-maps the same variables.

<html data-theme="light">…</html>

| Variable | Dark default (auto-light on [data-theme="light"]) | | ------------------------------------- | --------------------------------------------------- | | --atomic-editor-font | system sans | | --atomic-editor-font-mono | system mono | | --atomic-editor-body-size | 1.0625rem | | --atomic-editor-body-leading | 1.7 | | --atomic-editor-measure | 70ch | | --atomic-editor-fg | #dcddde | | --atomic-editor-fg-muted | #888 | | --atomic-editor-fg-faint | #666 | | --atomic-editor-bg | #1e1e1e | | --atomic-editor-bg-panel | #252525 | | --atomic-editor-bg-surface | #2d2d2d | | --atomic-editor-border | #3d3d3d | | --atomic-editor-accent | #7c3aed | | --atomic-editor-accent-bright | #a78bfa | | --atomic-editor-link | #60a5fa | | --atomic-editor-link-hover | #93c5fd | | --atomic-editor-code-bg | subtle dark panel | | --atomic-editor-selection-bg | accent-tinted 28% | | --atomic-editor-search-bg | accent-tinted 28% | | --atomic-editor-search-bg-active | accent-tinted 60% | | Code-token colors (Palenight) | | | --atomic-editor-hl-keyword | #c792ea | | --atomic-editor-hl-string | #c3e88d | | --atomic-editor-hl-number | #f78c6c | | --atomic-editor-hl-comment | #6a7a82 | | --atomic-editor-hl-type | #ffcb6b | | --atomic-editor-hl-function | #82aaff | | --atomic-editor-hl-property | #82aaff | | --atomic-editor-hl-regexp | #f07178 | | --atomic-editor-hl-escape | #89ddff | | --atomic-editor-hl-tag | #f07178 | | --atomic-editor-hl-variable | #eeffff | | --atomic-editor-hl-operator | #89ddff | | --atomic-editor-hl-invalid | #ff5370 |

Extending with plugins

CodeMirror 6 is extension-based, and so is this package. Pass any number of CM6 extensions via the extensions prop to layer in autocomplete sources, custom decorations, domain-specific keymaps, collaboration (yjs), vim mode, or anything else:

import { autocompletion, type CompletionContext } from '@codemirror/autocomplete';

const wikiLinks = autocompletion({
  override: [(ctx: CompletionContext) => {
    const match = ctx.matchBefore(/\[\[\w*$/);
    if (!match) return null;
    return {
      from: match.from + 2,
      options: myAtomStore.list().map((a) => ({ label: a.title })),
    };
  }],
});

<AtomicCodeMirrorEditor
  markdownSource={'…'}
  extensions={[wikiLinks]}
/>

Consumer extensions are appended after the built-ins, so wrap a custom keymap in Prec.high (from @codemirror/state) if it needs to beat the default bindings. The array is captured at mount — pass a stable reference unless you want a remount.

Low-level composition

If the React wrapper's extension set is too opinionated, every piece is exported individually so you can assemble a fully custom editor:

import {
  inlinePreview, // live preview decorations
  imageBlocks,   // rendered image widgets
  tables,        // WYSIWYG table widget
  atomicEditorTheme,
  atomicMarkdownSyntax,
  extendEmphasisPair,
} from '@atomic-editor/editor';

You could build an editor that includes inlinePreview() + tables() but skips atomicEditorTheme for your own EditorView.theme({...}), or swap atomicMarkdownSyntax for a custom syntaxHighlighting(HighlightStyle.define([...])). At that point you're outside the React wrapper and in plain CM6 territory.

Design notes

See docs/architecture.md for the full design rationale. Short version:

  • Raw markdown is the source of truth. All decorations are view-only — copy, save, and round-trip to any markdown parser are identical to what you'd expect from a plain textarea.
  • No layout shifts. Every line has a stable height regardless of cursor position. Inline decorations hide syntax tokens on inactive lines without changing line heights.
  • Narrow invalidation. Decoration rebuilds only touch lines whose content (or surrounding trigger characters) changed, so editing a paragraph in a 50KB doc costs O(change size), not O(doc).
  • Mouse-freeze guard. Clicks don't trigger a decoration rebuild mid-interaction — eliminates a class of cursor-drift bugs.
  • iOS-aware. Momentum scroll halts were investigated and fixed; the MinimalCodeMirrorEditor and NoPreviewCodeMirrorEditor flavors in the demo exist as bisection tools for any future scroll issues.

Contributing

git clone https://github.com/kenforthewin/atomic-editor
cd atomic-editor
npm install
npm run dev        # demo dev server at http://localhost:5173
npm test           # vitest unit tests
npm run build      # tsc emit to dist/
npm run test:e2e   # Playwright probe suite against the demo

The Playwright suite (scripts/test-editor.mjs) is the primary regression-catching tool — ~33 probes covering CLS during idle / scroll / typing, click-freeze timing, block-type decorations (headings, lists, tasks, tables, images, fences, HRs), copy-as-raw- markdown, tight-list continuation, escape handling, and late-doc rendering via the parser-progress mechanic. Run after any change to the editor's extensions.

Issues and PRs welcome.

License

MIT. See LICENSE.