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

@ashwin_droid/notion-stream

v1.2.0

Published

React component that converts streaming markdown into editable Notion-like blocks with real-time rendering

Readme

@ashwin_droid/notion-stream

A React component that converts streaming markdown into editable Notion-like blocks with real-time rendering. Perfect for AI chat interfaces, collaborative editors, and any application that needs to display and edit markdown content in a beautiful, block-based format.

Features

  • Streaming Support: Renders markdown in real-time as it streams in (perfect for LLM responses)
  • Notion-like Editing: Click-to-edit blocks with familiar keyboard shortcuts
  • Rich Block Types: Paragraphs, headings (H1-H6), lists, blockquotes, code blocks, tables, images, math (LaTeX), Mermaid diagrams, and dividers
  • Inline Formatting: Bold, italic, strikethrough, inline code, links, and inline math
  • Slash Commands: Type / to quickly insert any block type
  • Markdown Shortcuts: Type #, -, 1., >, ```, --- etc. to convert blocks
  • Dark Mode: Built-in light/dark theme support with system preference detection
  • Diff Review: Built-in utilities for comparing and reviewing markdown changes
  • Fully Typed: Complete TypeScript support with exported types
  • Customizable: Hooks for custom block rendering and slash commands

Installation

npm install @ashwin_droid/notion-stream
# or
yarn add @ashwin_droid/notion-stream
# or
pnpm add @ashwin_droid/notion-stream

Quick Start

import { NotionStream } from '@ashwin_droid/notion-stream'
import '@ashwin_droid/notion-stream/styles.css'

function App() {
  const [markdown, setMarkdown] = useState('# Hello World\n\nStart typing...')

  return (
    <NotionStream
      markdown={markdown}
      onMarkdownChange={setMarkdown}
    />
  )
}

Usage with Streaming (AI Chat)

import { NotionStream } from '@ashwin_droid/notion-stream'
import '@ashwin_droid/notion-stream/styles.css'

function ChatMessage({ stream }: { stream: AsyncIterable<string> }) {
  const [markdown, setMarkdown] = useState('')
  const [isStreaming, setIsStreaming] = useState(true)

  useEffect(() => {
    async function consume() {
      for await (const chunk of stream) {
        setMarkdown(prev => prev + chunk)
      }
      setIsStreaming(false)
    }
    consume()
  }, [stream])

  return (
    <NotionStream
      markdown={markdown}
      isStreaming={isStreaming}
      onMarkdownChange={setMarkdown}
    />
  )
}

Props

NotionStreamProps

| Prop | Type | Default | Description | |------|------|---------|-------------| | markdown | string | required | The markdown content to render | | isStreaming | boolean | false | Whether content is currently streaming in | | onMarkdownChange | (markdown: string) => void | - | Callback when content is edited | | className | string | '' | Additional CSS classes | | theme | 'light' \| 'dark' \| 'system' | auto-detect | Theme mode | | readonly | boolean | false | Disable editing (viewer mode) | | hooks | BlockHooks | - | Custom block hooks (see below) |

Block Types

NotionStream supports the following block types:

| Block | Markdown Syntax | Description | |-------|----------------|-------------| | Paragraph | Plain text | Default text block | | Heading 1-6 | # to ###### | Section headings | | Bulleted List | - or * | Unordered list items | | Numbered List | 1. | Ordered list items | | Blockquote | > | Quote blocks | | Code Block | ``` | Syntax-highlighted code | | Math Block | $$ | LaTeX equations (KaTeX) | | Mermaid | ```mermaid | Diagrams and flowcharts | | Table | \| ... \| | Markdown tables | | Image | ![alt](url) | Image blocks | | Divider | --- | Horizontal rule |

Inline Formatting

| Style | Markdown | Result | |-------|----------|--------| | Bold | **text** | text | | Italic | *text* or _text_ | text | | Strikethrough | ~~text~~ | ~~text~~ | | Inline Code | `code` | code | | Link | [text](url) | text | | Inline Math | $x^2$ | Rendered LaTeX |

Keyboard Shortcuts

Block Operations

  • Enter - Create new block below (in paragraphs/lists)
  • Backspace at start - Delete empty block or merge with previous
  • Tab / Shift+Tab - Indent/outdent list items
  • / on empty block - Open slash command menu

Markdown Shortcuts (type at start of empty block)

  • # - Heading 1
  • ## - Heading 2
  • ### - Heading 3
  • - or * - Bulleted list
  • 1. - Numbered list
  • > - Blockquote
  • ``` - Code block
  • --- - Divider

Ref API

Access the imperative API using a ref:

import { useRef } from 'react'
import { NotionStream, NotionStreamRef } from '@ashwin_droid/notion-stream'

function Editor() {
  const ref = useRef<NotionStreamRef>(null)

  const addCodeBlock = () => {
    ref.current?.insertBlockAfter(null, {
      type: 'code',
      language: 'typescript',
      content: '// Your code here'
    })
  }

  return (
    <>
      <button onClick={addCodeBlock}>Add Code Block</button>
      <NotionStream ref={ref} markdown="" />
    </>
  )
}

NotionStreamRef Methods

| Method | Signature | Description | |--------|-----------|-------------| | updateBlock | (blockId: string, value: string \| Span[]) => void | Update block content | | getBlock | (blockId: string) => Block \| null | Get block by ID | | getAllBlocks | () => Block[] | Get all blocks | | getMarkdown | () => string | Get current markdown | | insertBlockAfter | (blockId: string \| null, block: BlockInsert) => string | Insert new block | | deleteBlock | (blockId: string) => void | Delete a block | | setBlockType | (blockId: string, type: BlockType, options?) => void | Change block type | | indentListItem | (blockId: string) => void | Indent list item | | outdentListItem | (blockId: string) => void | Outdent list item |

Custom Block Rendering

Use hooks to customize block rendering:

import { NotionStream, BlockHooks, Block } from '@ashwin_droid/notion-stream'

const hooks: BlockHooks = {
  renderBlock: (block: Block, defaultRender: () => React.ReactNode) => {
    // Custom rendering for specific blocks
    if (block.type === 'code' && block.language === 'custom') {
      return <MyCustomCodeBlock code={block.content} />
    }
    // Fall back to default rendering
    return defaultRender()
  },

  onBlockChange: (blockId: string, newValue: string | Span[], block: Block) => {
    console.log('Block changed:', blockId, newValue)
  }
}

function Editor() {
  return <NotionStream markdown="" hooks={hooks} />
}

Custom Slash Commands

Add your own slash commands:

const hooks: BlockHooks = {
  getSlashCommands: ({ block }) => [
    {
      id: 'callout',
      title: 'Callout',
      description: 'Highlighted callout box',
      group: 'Custom',
      aliases: ['info', 'warning'],
      icon: <InfoIcon />,
      execute: (api, { blockId }) => {
        api.setBlockType(blockId, 'blockquote')
        api.updateBlock(blockId, [{ text: 'Callout content', style: 'plain' }])
      }
    }
  ]
}

Diff Review Utilities

Compare and review markdown changes:

import { useDiffReview } from '@ashwin_droid/notion-stream'

function DiffReviewer({ original, modified }: { original: string; modified: string }) {
  const {
    items,          // Array of diff items
    decisions,      // Current accept/reject decisions
    accept,         // Accept a change
    reject,         // Reject a change
    resultMarkdown, // Final markdown after applying decisions
    stats           // { added, removed, modified, accepted, rejected, pending }
  } = useDiffReview({
    baseMarkdown: original,
    nextMarkdown: modified,
    defaultDecision: 'unset'
  })

  return (
    <div>
      {items.map(item => (
        <div key={item.id}>
          {item.type === 'added' && <span className="text-green-500">+ {item.markdown}</span>}
          {item.type === 'removed' && <span className="text-red-500">- {item.markdown}</span>}
          {item.type !== 'unchanged' && (
            <>
              <button onClick={() => accept(item.id)}>Accept</button>
              <button onClick={() => reject(item.id)}>Reject</button>
            </>
          )}
        </div>
      ))}
    </div>
  )
}

Diff Utilities

| Function | Description | |----------|-------------| | computeDiff(a, b) | Word-level diff between strings | | computeCodeDiff(a, b) | Line-level diff for code with inline highlights | | computeMarkdownBlockDiff(a, b) | Block-level markdown diff | | computeMarkdownDiffReviewItems(a, b) | Structured diff for review UI | | applyMarkdownDiffReview(items, decisions) | Apply review decisions to get final markdown |

Low-Level Hooks

For advanced use cases, individual hooks are exported:

import {
  useStreamParser,       // Parse markdown into blocks
  useBlockState,         // Manage block state
  useMarkdownShortcuts,  // Handle markdown shortcuts
  useDiffReview          // Diff review utilities
} from '@ashwin_droid/notion-stream'

Span Utilities

Work with inline formatting:

import {
  parseMarkdownToSpans,  // Parse inline markdown to spans
  spansToMarkdown,       // Convert spans back to markdown
  spansToHtml,           // Convert spans to HTML
  renderSpans,           // Render spans as React nodes
  applyStyleToRange,     // Apply style to text range
  toggleStyle            // Toggle inline style
} from '@ashwin_droid/notion-stream'

Block Components

Individual block components are exported for custom layouts:

import {
  ParagraphBlock,
  HeadingBlock,
  CodeBlock,
  ImageBlock,
  MathBlock,
  MermaidBlock,
  ListBlock,
  BlockquoteBlock,
  DividerBlock
} from '@ashwin_droid/notion-stream'

Theming

Auto Theme Detection

By default, NotionStream detects the theme from:

  1. .dark class on <html> or <body> elements
  2. System preference via prefers-color-scheme

Explicit Theme

<NotionStream markdown="" theme="dark" />
<NotionStream markdown="" theme="light" />
<NotionStream markdown="" theme="system" />

CSS Variables

Customize colors using CSS variables:

.notion-stream {
  --notion-bg: #ffffff;
  --notion-text: #1f2937;
  --notion-text-secondary: #6b7280;
  --notion-border: #e5e7eb;
  /* ... see styles.css for all variables */
}

.notion-stream.dark {
  --notion-bg: #1f2937;
  --notion-text: #f3f4f6;
  /* ... */
}

TypeScript Types

All types are exported:

import type {
  Block,
  BlockType,
  InlineBlock,
  Span,
  InlineStyle,
  ListType,
  BlockInsert,
  SlashCommand,
  NotionStreamProps,
  NotionStreamRef,
  BlockHooks,
  BlockProps,
  Theme,
  TableAlign,
  TableBlock,
  DiffSegment,
  CodeDiff,
  MarkdownDiffReviewItem,
  DiffReviewDecision
} from '@ashwin_droid/notion-stream'

Peer Dependencies

This package requires:

  • react >= 18.0.0
  • react-dom >= 18.0.0
  • streamdown >= 2.0.0
  • @streamdown/code >= 1.0.0
  • @streamdown/math >= 1.0.0 (includes katex)
  • @streamdown/mermaid >= 1.0.0
  • tailwindcss >= 3.0.0
  • @radix-ui/react-scroll-area >= 1.0.0
npm install react react-dom streamdown @streamdown/code @streamdown/math @streamdown/mermaid tailwindcss @radix-ui/react-scroll-area

Note: You must also import the KaTeX CSS for math rendering:

import 'katex/dist/katex.min.css'

Dependencies

NotionStream uses:

Browser Support

  • Chrome/Edge (latest)
  • Firefox (latest)
  • Safari (latest)

License

MIT

Contributing

Contributions are welcome! Please open an issue or submit a pull request.

Acknowledgments

Inspired by Notion's block-based editing experience.