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

authorly-editor

v0.1.8

Published

A rich text editor for authors, blogs, and documentation with clean, publish-ready output.

Readme


Why Authorly?

| Feature | Authorly | Other Editors | |---------|----------|---------------| | Output | Pure semantic HTML | JSON AST / Custom format | | Dependencies | React + Lucide icons | Heavy frameworks | | Bundle size | ~30kb gzipped | 100kb+ | | Learning curve | Minutes | Hours/Days | | Database storage | Just HTML string | Complex serialization |

<!-- What you get: Clean, portable HTML ready to publish -->
<h1>My Article</h1>
<p>A paragraph with <strong>bold</strong> and <em>italic</em> text.</p>
<ul>
  <li>Simple</li>
  <li>Clean</li>
  <li>Works everywhere</li>
</ul>

Installation

npm install authorly-editor
yarn add authorly-editor
pnpm add authorly-editor

Quick Start

import { ContentBlocksEditor } from 'authorly-editor';

function App() {
  const [content, setContent] = useState('<p>Hello World!</p>');

  return (
    <ContentBlocksEditor
      initialContent={content}
      onChange={setContent}
    />
  );
}

That's it. No configuration needed.


Components

1. ContentBlocksEditor

The main editor component for creating and editing content.

import { ContentBlocksEditor } from 'authorly-editor';

<ContentBlocksEditor
  initialContent="<p>Start writing...</p>"
  onChange={(html) => console.log(html)}
  onSave={(html) => saveToDatabase(html)}
  darkMode={false}
  showToolbar={true}
  placeholder="Type '/' for commands..."
/>

2. ContentBlocksRenderer

Display saved HTML content with beautiful styling. No editor overhead.

import { ContentBlocksRenderer } from 'authorly-editor';

<ContentBlocksRenderer
  html={savedContent}
  darkMode={false}
  enableCodeCopy={true}
/>

3. TableOfContents

Auto-generate navigation from your content headings.

import { TableOfContents, ContentBlocksRenderer } from 'authorly-editor';

<div style={{ display: 'flex' }}>
  <aside style={{ width: 200 }}>
    <TableOfContents html={content} title="Contents" />
  </aside>
  <main>
    <ContentBlocksRenderer html={content} enableHeadingIds={true} />
  </main>
</div>

Block Types

| Block | Description | HTML Output | |-------|-------------|-------------| | Paragraph | Basic text | <p> | | Heading 1-6 | Section headings | <h1> - <h6> | | Bullet List | Unordered list | <ul><li> | | Numbered List | Ordered list | <ol><li> | | Checklist | Todo items | <ul><li><input type="checkbox"> | | Quote | Blockquote | <blockquote> | | Code | Code block | <pre><code> | | Image | Image with caption | <figure><img><figcaption> | | Video | YouTube/Vimeo/MP4 | <figure><iframe> | | Table | Data table | <table> | | Divider | Horizontal rule | <hr> | | Callout | Info/Warning/Error | <aside> | | Accordion | Collapsible section | <details><summary> |


Keyboard Shortcuts

| Shortcut | Action | |----------|--------| | Ctrl/Cmd + B | Bold | | Ctrl/Cmd + I | Italic | | Ctrl/Cmd + U | Underline | | Ctrl/Cmd + S | Save (triggers onSave) | | Ctrl/Cmd + Z | Undo | | Ctrl/Cmd + Y | Redo | | Ctrl/Cmd + 1/2/3 | Heading 1/2/3 | | / | Open block menu | | Enter | New block / New list item | | Backspace | Delete empty block / Merge | | Tab | Indent list / Navigate table | | ↑ / ↓ | Navigate between blocks |


API Reference

ContentBlocksEditor Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | initialContent | string | '' | Initial HTML content | | onChange | (html: string) => void | - | Called on content change | | onSave | (html: string) => void | - | Called on Ctrl+S | | onFocus | () => void | - | Called when editor gains focus | | onBlur | () => void | - | Called when editor loses focus | | onReady | (editor: EditorInstance) => void | - | Called when editor is ready | | darkMode | boolean | false | Enable dark theme | | showToolbar | boolean | true | Show formatting toolbar | | toolbarPosition | 'top' \| 'bottom' | 'top' | Toolbar position | | placeholder | string | 'Type "/" for commands...' | Placeholder text | | readOnly | boolean | false | Disable editing | | autoFocus | boolean | false | Focus on mount | | spellCheck | boolean | true | Enable spell check | | className | string | '' | Custom class name | | style | CSSProperties | - | Custom styles |

EditorRef Methods

Access editor methods using a ref:

import { useRef } from 'react';
import { ContentBlocksEditor, EditorRef } from 'authorly-editor';

function MyEditor() {
  const editorRef = useRef<EditorRef>(null);

  return (
    <>
      <ContentBlocksEditor ref={editorRef} />
      <button onClick={() => console.log(editorRef.current?.getHTML())}>
        Get HTML
      </button>
    </>
  );
}

| Method | Description | |--------|-------------| | getHTML() | Returns the current HTML content | | setHTML(html: string) | Sets the editor content | | getText() | Returns plain text content | | focus() | Focuses the editor | | blur() | Blurs the editor | | insertBlock(type, data?) | Inserts a new block | | getEditor() | Returns the full editor instance |

ContentBlocksRenderer Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | html | string | '' | HTML content to render | | darkMode | boolean | false | Enable dark theme | | enableCodeCopy | boolean | true | Add copy button to code blocks | | enableHeadingIds | boolean | true | Add IDs to headings | | enableChecklistStyles | boolean | true | Strikethrough checked items | | className | string | '' | Custom class name | | style | CSSProperties | - | Custom styles |

TableOfContents Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | html | string | '' | HTML to extract headings from | | darkMode | boolean | false | Enable dark theme | | title | string | 'Table of Contents' | Title text | | minLevel | number | 1 | Min heading level (1-6) | | maxLevel | number | 6 | Max heading level (1-6) | | onNavigate | (id, item) => void | - | Custom navigation handler | | smoothScroll | boolean | true | Smooth scroll to heading | | collapsible | boolean | false | Make TOC collapsible |


Examples

Blog Editor with Preview

import { useState, useRef } from 'react';
import { 
  ContentBlocksEditor, 
  ContentBlocksRenderer,
  EditorRef 
} from 'authorly-editor';

function BlogEditor() {
  const editorRef = useRef<EditorRef>(null);
  const [content, setContent] = useState('<p>Write your post...</p>');
  const [showPreview, setShowPreview] = useState(false);

  const handleSave = async (html: string) => {
    await fetch('/api/posts', {
      method: 'POST',
      body: JSON.stringify({ content: html }),
    });
  };

  return (
    <div>
      <button onClick={() => setShowPreview(!showPreview)}>
        {showPreview ? 'Edit' : 'Preview'}
      </button>

      {showPreview ? (
        <ContentBlocksRenderer html={content} />
      ) : (
        <ContentBlocksEditor
          ref={editorRef}
          initialContent={content}
          onChange={setContent}
          onSave={handleSave}
        />
      )}
    </div>
  );
}

Documentation Page with TOC

import { 
  ContentBlocksRenderer, 
  TableOfContents 
} from 'authorly-editor';

function DocsPage({ content }) {
  return (
    <div style={{ display: 'grid', gridTemplateColumns: '250px 1fr', gap: '2rem' }}>
      <aside style={{ position: 'sticky', top: '1rem', height: 'fit-content' }}>
        <TableOfContents 
          html={content} 
          title="On this page"
          maxLevel={3}
        />
      </aside>
      <main>
        <ContentBlocksRenderer 
          html={content}
          enableHeadingIds={true}
          enableCodeCopy={true}
        />
      </main>
    </div>
  );
}

Dark Mode Support

import { useState } from 'react';
import { ContentBlocksEditor } from 'authorly-editor';

function ThemedEditor() {
  const [darkMode, setDarkMode] = useState(false);

  return (
    <div style={{ 
      background: darkMode ? '#0f172a' : '#ffffff',
      minHeight: '100vh',
      padding: '2rem'
    }}>
      <button onClick={() => setDarkMode(!darkMode)}>
        Toggle Theme
      </button>
      <ContentBlocksEditor darkMode={darkMode} />
    </div>
  );
}

Customization

CSS Variables

Override these CSS variables to customize the editor appearance:

.cb-editor {
  /* Colors */
  --cb-primary: #3b82f6;
  --cb-primary-hover: #2563eb;
  --cb-bg: #ffffff;
  --cb-bg-secondary: #f9fafb;
  --cb-bg-tertiary: #f3f4f6;
  --cb-text: #111827;
  --cb-text-secondary: #6b7280;
  --cb-border: #e5e7eb;
  --cb-border-focus: #3b82f6;

  /* Spacing */
  --cb-spacing-xs: 0.25rem;
  --cb-spacing-sm: 0.5rem;
  --cb-spacing-md: 1rem;
  --cb-spacing-lg: 1.5rem;

  /* Border radius */
  --cb-radius-sm: 0.25rem;
  --cb-radius-md: 0.375rem;
  --cb-radius-lg: 0.5rem;

  /* Typography */
  --cb-font-family: system-ui, -apple-system, sans-serif;
  --cb-font-mono: 'SF Mono', Monaco, Consolas, monospace;
}

Custom Blocks

Register your own block types:

import { blockRegistry, BlockDefinition } from 'authorly-editor';

const myCustomBlock: BlockDefinition = {
  name: 'custom',
  tag: 'div',
  editable: true,
  allowedChildren: ['text', 'inline'],
  label: 'Custom Block',
  icon: 'box',
  create: (data) => {
    const el = document.createElement('div');
    el.className = 'my-custom-block';
    el.contentEditable = 'true';
    el.innerHTML = data?.content || '';
    return el;
  },
  getData: (el) => ({ content: el.innerHTML }),
  update: (el, data) => { el.innerHTML = data.content; },
};

blockRegistry.register(myCustomBlock);

Browser Support

| Browser | Version | |---------|---------| | Chrome | 90+ | | Firefox | 90+ | | Safari | 14+ | | Edge | 90+ |


TypeScript

Full TypeScript support with exported types:

import type {
  EditorRef,
  EditorInstance,
  BlockType,
  BlockData,
  ContentBlocksEditorProps,
  ContentBlocksRendererProps,
  TableOfContentsProps,
  TocItem,
} from 'authorly-editor';

FAQ

The editor outputs plain HTML strings. Save it directly:

const handleSave = async (html: string) => {
  await db.posts.create({ content: html });
};

<ContentBlocksEditor onSave={handleSave} />

Use the ContentBlocksRenderer component:

const post = await db.posts.findOne(id);

<ContentBlocksRenderer html={post.content} />

Currently, Authorly is React-only. The output HTML can be used anywhere, but the editor component requires React 17+.

Not built-in. For real-time collaboration, you'd need to integrate with a service like Yjs or Liveblocks on top of this editor.

The editor supports pasting images (as base64) and entering URLs. For server uploads, handle it in your app:

const handleImageUpload = async (file: File) => {
  const url = await uploadToS3(file);
  editorRef.current?.insertBlock('image', { src: url });
};

Contributing

Contributions are welcome! Please read our contributing guidelines first.

# Clone the repo
git clone https://github.com/your-username/authorly.git

# Install dependencies
npm install

# Start dev server
npm run dev

# Run tests
npm test

# Build
npm run build

License

MIT © Aaditya Hasabnis