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

rayo-editor

v0.0.6

Published

Standalone npm module for Rayo Blog Editor

Readme

rayo-editor

npm version license coverage

A professional-grade rich text editor component for React, built with TipTap and optimized for blog content creation. Drop-in replacement for BlogEditor with advanced diff highlighting, collaborative editing support, and real-time content processing.

Features

  • Rich Text Editing - Full-featured editor with support for headings, lists, code blocks, tables, and images
  • Diff Highlighting - Visual diff overlays showing changes in green (additions) and red (deletions)
  • Review UI - Accept/reject individual changes or bulk approve all modifications
  • Image Handling - Integrated image management with generation loader
  • TypeScript Support - Fully typed API with comprehensive JSDoc documentation
  • React 18+ - Modern React patterns with hooks and ref forwarding
  • Zero Configuration - Works out of the box, no setup required
  • Rive Animations - Smooth loading and generation states
  • 96.3% Test Coverage - Comprehensive test suite with 271+ tests

Installation

npm install rayo-editor
# or
yarn add rayo-editor
# or
pnpm add rayo-editor

Quick Start

Basic Usage

import React, { useRef } from 'react';
import { RayoEditor, BlogSimpleEditorRef } from 'rayo-editor';
import 'rayo-editor/styles';

export function MyBlogEditor() {
  const editorRef = useRef<BlogSimpleEditorRef>(null);
  const [content, setContent] = React.useState('');
  const [title, setTitle] = React.useState('');

  return (
    <RayoEditor
      content={content}
      title={title}
      onChange={setContent}
      onTitleChange={setTitle}
      isLoading={false}
      editorRef={editorRef}
    />
  );
}

With Diff Highlighting

<RayoEditor
  content={content}
  title={title}
  onChange={setContent}
  onTitleChange={setTitle}
  showDiffs={true}
  pendingChanges={true}
  onAcceptChanges={() => {
    // Handle accepting all changes
    console.log('Changes accepted');
  }}
  onRejectChanges={() => {
    // Handle rejecting all changes
    console.log('Changes rejected');
  }}
  isLoading={false}
  editorRef={editorRef}
/>

Read-Only Mode

<RayoEditor
  content={publishedContent}
  title={publishedTitle}
  onChange={() => {}} // No-op
  readOnly={true}
  isLoading={false}
  editorRef={editorRef}
/>

With Callbacks

<RayoEditor
  content={content}
  title={title}
  onChange={setContent}
  onTitleChange={setTitle}
  onAcceptSingleChange={(greenRange, redRange) => {
    console.log('Single change accepted', greenRange, redRange);
  }}
  onRejectSingleChange={(greenRange, redRange) => {
    console.log('Single change rejected', greenRange, redRange);
  }}
  onDiffPairsChange={(pairs) => {
    console.log('Diff pairs updated', pairs);
  }}
  isLoading={false}
  editorRef={editorRef}
/>

API Reference

Components

RayoEditor

Main editor component providing a complete rich text editing experience.

<RayoEditor {...props} />

Props:

interface RayoEditorProps {
  // Content management
  content: string;
  title: string;
  onChange: (content: string) => void;
  onTitleChange?: (title: string) => void;

  // State management
  isLoading: boolean;
  isStreaming?: boolean;
  isAgentThinking?: boolean;
  readOnly?: boolean;
  focusMode?: boolean;

  // Diff & Review features
  pendingChanges?: boolean;
  showDiffs?: boolean;
  hideReviewUI?: boolean;
  editedLinesCount?: number;
  onAcceptChanges?: () => void;
  onRejectChanges?: () => void;
  onAcceptSingleChange?: (greenRange: DiffRange, redRange?: DiffRange) => void;
  onRejectSingleChange?: (greenRange: DiffRange, redRange?: DiffRange) => void;
  onDiffPairsChange?: (diffPairs: DiffPair[]) => void;

  // Image management
  featuredImageUrl?: string;
  onEditFeaturedImage?: () => void;
  isGeneratingImage?: boolean;

  // Advanced options
  editorRef: RefObject<BlogSimpleEditorRef>;
  onAriScoreChange?: (score: number) => void;
  disableAutoScroll?: boolean;
  onUserScrollChange?: (isScrolledUp: boolean) => void;
  showToolbarAnimation?: boolean;
  streamingPhase?: string;
}

BlogEditor

Legacy component wrapper - identical to RayoEditor, maintained for backward compatibility.

import { BlogEditor } from 'rayo-editor';

TitleTextarea

Standalone title input component.

<TitleTextarea
  title={title}
  onTitleChange={setTitle}
  readOnly={false}
/>

DiffOverlay

Visual overlay component for displaying diff highlights.

<DiffOverlay
  diffPairs={diffPairs}
  overlayHoleRect={rect}
  onPairHover={(index) => console.log(index)}
  onPairClick={(index) => console.log(index)}
/>

ReviewButtons

Accept/reject buttons for review operations.

<ReviewButtons
  onAccept={() => console.log('Accepted')}
  onReject={() => console.log('Rejected')}
  position={{ top: 100, left: 50 }}
/>

Hooks

useEditorDiff

Hook for managing diff detection and overlay state.

const {
  diffPairs,
  setDiffPairs,
  activePairIndex,
  setActivePairIndex,
  hoverPairIndex,
  setHoverPairIndex,
  overlayHoleRect,
  setOverlayHoleRect,
  updateDiffRanges
} = useEditorDiff(editorRef, { focusMode: false });

Returns:

  • diffPairs: DiffPair[] - Array of detected diff pairs
  • activePairIndex: number - Currently active pair index
  • hoverPairIndex: number - Currently hovered pair index
  • overlayHoleRect: Rect | null - Rectangle for overlay positioning
  • updateDiffRanges: () => void - Function to trigger diff update

useContentProcessing

Hook for content transformation and processing.

const {
  content,
  setContent,
  isProcessing,
  error,
  processContent
} = useContentProcessing(initialContent);

Returns:

  • content: string - Current processed content
  • setContent: (content: string) => void - Direct content setter
  • isProcessing: boolean - Processing state flag
  • error: Error | null - Last processing error
  • processContent: (content: string) => void - Process and update content

Utility Functions

Diff Detection

// Detect diff markers in content
detectDiffMarkers(content: string): DiffMarkerResult;

// Normalize diff text for comparison
normalizeDiffText(text: string): string;

// Extract diff ranges from content
extractDiffRanges(content: string, type?: 'all' | 'green' | 'red'): DiffRange[];

Content Processing

// Merge consecutive or overlapping ranges
mergeConsecutiveRanges(ranges: DiffRange[]): DiffRange[];

// Extract plain text from HTML content
extractTextContent(html: string): string;

// Optimize ranges for rendering
optimizeRanges(ranges: DiffRange[]): DiffRange[];

Image Handling

// Group consecutive images in content
groupConsecutiveImages(content: string): ImageOperation[];

// Match image replacements in diff pairs
matchImageReplacements(content: string): Array<{old: string, new: string}>;

Proximity Matching

// Calculate proximity score between text segments
calculateProximity(text1: string, text2: string): number;

// Find the best matching text pair
findOwnerTextPair(target: string, candidates: string[]): string | null;

// Group items by proximity
groupConsecutiveItems<T>(items: T[], key: string): T[][];

Error Handling

// Safe execution wrapper
safeExecute<T>(fn: () => T, errorHandler?: (error: Error) => void): T | null;

// Custom error class
class DiffProcessingError extends Error {
  constructor(message: string, public originalError?: Error) {}
}

Props Documentation

RayoEditorProps

All props are documented below with their purposes and default values:

| Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | content | string | Yes | - | Current editor content (HTML) | | title | string | Yes | - | Blog post title | | onChange | (content: string) => void | Yes | - | Callback fired on content changes | | onTitleChange | (title: string) => void | No | - | Callback fired on title changes | | isLoading | boolean | Yes | - | Shows loading state | | editorRef | RefObject<BlogSimpleEditorRef> | Yes | - | Reference to editor instance | | isStreaming | boolean | No | false | Indicates streaming in progress | | isAgentThinking | boolean | No | false | Shows agent thinking state | | readOnly | boolean | No | false | Disables editing | | focusMode | boolean | No | false | Minimal UI focus mode | | pendingChanges | boolean | No | false | Indicates pending changes exist | | showDiffs | boolean | No | false | Show diff highlighting | | hideReviewUI | boolean | No | false | Hide review buttons | | editedLinesCount | number | No | - | Number of edited lines | | onAcceptChanges | () => void | No | - | Accept all changes callback | | onRejectChanges | () => void | No | - | Reject all changes callback | | onAcceptSingleChange | (green, red?) => void | No | - | Accept single change | | onRejectSingleChange | (green, red?) => void | No | - | Reject single change | | onDiffPairsChange | (pairs) => void | No | - | Diff pairs updated | | featuredImageUrl | string | No | - | Featured image URL | | onEditFeaturedImage | () => void | No | - | Edit featured image callback | | isGeneratingImage | boolean | No | false | Image generation in progress | | onAriScoreChange | (score: number) => void | No | - | ARI score updated | | disableAutoScroll | boolean | No | false | Disable auto-scroll | | onUserScrollChange | (scrolledUp: boolean) => void | No | - | User scroll state | | showToolbarAnimation | boolean | No | true | Show toolbar animations | | streamingPhase | string | No | - | Current streaming phase |

Examples

Advanced: Collaborative Editing

import React, { useRef, useState } from 'react';
import { RayoEditor, BlogSimpleEditorRef, useEditorDiff } from 'rayo-editor';

export function CollaborativeEditor() {
  const editorRef = useRef<BlogSimpleEditorRef>(null);
  const [content, setContent] = useState('');
  const [title, setTitle] = useState('');
  const [pendingChanges, setPendingChanges] = useState(false);

  const { diffPairs, updateDiffRanges } = useEditorDiff(editorRef);

  const handleAcceptChanges = async () => {
    // Apply changes and send to server
    await fetch('/api/changes/accept', {
      method: 'POST',
      body: JSON.stringify({ content, diffPairs })
    });
    setPendingChanges(false);
  };

  return (
    <RayoEditor
      content={content}
      title={title}
      onChange={setContent}
      onTitleChange={setTitle}
      showDiffs={pendingChanges}
      pendingChanges={pendingChanges}
      onAcceptChanges={handleAcceptChanges}
      onRejectChanges={() => setContent(content)}
      isLoading={false}
      editorRef={editorRef}
    />
  );
}

Advanced: With Content Processing

import { RayoEditor, useContentProcessing } from 'rayo-editor';

export function SmartEditor() {
  const editorRef = useRef<BlogSimpleEditorRef>(null);
  const {
    content,
    setContent,
    isProcessing,
    error,
    processContent
  } = useContentProcessing('');

  const handleAIEnhance = async () => {
    const enhanced = await fetch('/api/enhance', {
      method: 'POST',
      body: JSON.stringify({ content })
    }).then(r => r.json());

    processContent(enhanced.content);
  };

  return (
    <>
      <RayoEditor
        content={content}
        title=""
        onChange={processContent}
        isLoading={isProcessing}
        editorRef={editorRef}
      />
      {error && <div className="error">{error.message}</div>}
      <button onClick={handleAIEnhance} disabled={isProcessing}>
        {isProcessing ? 'Processing...' : 'Enhance with AI'}
      </button>
    </>
  );
}

Advanced: Real-time Diff Highlighting

import React, { useRef, useState, useEffect } from 'react';
import { RayoEditor, useEditorDiff, detectDiffMarkers } from 'rayo-editor';

export function RealtimeDiffEditor() {
  const editorRef = useRef<BlogSimpleEditorRef>(null);
  const [content, setContent] = useState('');
  const { diffPairs, setDiffPairs } = useEditorDiff(editorRef);

  useEffect(() => {
    const { hasDiffs } = detectDiffMarkers(content);
    if (hasDiffs) {
      // Process and set diff pairs
      setDiffPairs([]);
    }
  }, [content, setDiffPairs]);

  return (
    <RayoEditor
      content={content}
      title=""
      onChange={setContent}
      showDiffs={diffPairs.length > 0}
      isLoading={false}
      editorRef={editorRef}
    />
  );
}

Architecture

Component Hierarchy

RayoEditor (Main Component)
├── BlogEditor (Core Editor Implementation)
├── TitleTextarea (Title Input)
├── DiffOverlay (Diff Visualization)
├── ReviewButtons (Accept/Reject Controls)
└── ImageGenerationLoader (Image Loading State)

Hook System

  • useEditorDiff - Manages diff state and detection
  • useContentProcessing - Handles content transformation
  • Internal hooks - TipTap editor instance management

Utility Pipeline

Content Input
    ↓
Diff Detection (detectDiffMarkers)
    ↓
Normalization (normalizeDiffText)
    ↓
Range Extraction (extractDiffRanges)
    ↓
Range Optimization (mergeConsecutiveRanges, optimizeRanges)
    ↓
Rendering (DiffOverlay)

Testing

The package includes comprehensive tests covering all functionality:

# Run all tests
npm run test

# Run with UI
npm run test:ui

# Generate coverage report
npm run test:coverage

Test Statistics:

  • 271+ test cases
  • 96.3% code coverage
  • All critical paths covered
  • Edge cases and error scenarios tested

Contributing

Contributions are welcome! Please follow these guidelines:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Make your changes with tests
  4. Ensure all tests pass (npm run test)
  5. Commit with clear messages (git commit -m 'feat: add amazing feature')
  6. Push to the branch (git push origin feature/amazing-feature)
  7. Open a Pull Request

Code Standards

  • Use TypeScript for type safety
  • Add JSDoc comments for public APIs
  • Write tests for new features
  • Maintain >95% code coverage
  • Follow eslint configuration

Changelog

See CHANGELOG.md for version history and release notes.

License

MIT License - see LICENSE file for details.


Built with ❤️ for content creators