rayo-editor
v0.0.6
Published
Standalone npm module for Rayo Blog Editor
Readme
rayo-editor
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-editorQuick 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 pairsactivePairIndex: number- Currently active pair indexhoverPairIndex: number- Currently hovered pair indexoverlayHoleRect: Rect | null- Rectangle for overlay positioningupdateDiffRanges: () => 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 contentsetContent: (content: string) => void- Direct content setterisProcessing: boolean- Processing state flagerror: Error | null- Last processing errorprocessContent: (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:coverageTest 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:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes with tests
- Ensure all tests pass (
npm run test) - Commit with clear messages (
git commit -m 'feat: add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - 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
