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

eddyter

v1.4.19

Published

Plug and Play AI Rich Text Editor for any website, blog, CRM, ERP, or web app. Built on Lexical with AI writing assistance, dark mode, and real-time collaboration.

Downloads

1,916

Readme

Eddyter

npm version npm downloads Bundle Size

Plug and Play AI Rich Text Editor for any website, blog, CRM, ERP, or web app — built on Lexical with dark mode support and API key authentication.

Eddyter Editor

Resources

Installation

npm install eddyter
# or
yarn add eddyter
# or
pnpm add eddyter

Compatibility

| Requirement | Version | |-------------|---------| | React | 18.2+ or 19.x | | React DOM | 18.2+ or 19.x | | Node.js | 16+ |

Quick Start

1. Import styles

import 'eddyter/style.css';

Important: The stylesheet is required for tables, toolbars, and all editor components to render correctly.

2. Get your API key

  1. Create an account at eddyter.com
  2. Navigate to License Keys in your dashboard
  3. Copy your API key

3. Add the editor

import React from 'react';
import {
  ConfigurableEditorWithAuth,
  EditorProvider,
  defaultEditorConfig
} from 'eddyter';
import 'eddyter/style.css';

function App() {
  const apiKey = process.env.NEXT_PUBLIC_EDITOR_API_KEY!;

  const currentUser = {
    id: 'user-123',
    name: 'John Doe',
    email: '[email protected]',
    avatar: 'https://example.com/avatar.jpg' // optional
  };

  return (
    <EditorProvider
      defaultFontFamilies={defaultEditorConfig.defaultFontFamilies}
      currentUser={currentUser}
    >
      <ConfigurableEditorWithAuth
        apiKey={apiKey}
        onChange={(html) => console.log('Content:', html)}
        initialContent="<p>Start writing...</p>"
        mentionUserList={['Alice', 'Bob', 'Charlie']}
        onAuthSuccess={() => console.log('Editor ready!')}
        onAuthError={(error) => console.error('Auth failed:', error)}
      />
    </EditorProvider>
  );
}

Features

Text & Formatting

  • Bold, italic, underline, strikethrough, subscript, superscript
  • Text color and background highlight with color picker
  • 20+ font families with adjustable font sizes
  • Text alignment (left, center, right, justify)
  • Line height and letter spacing controls

Lists & Structure

  • Bullet lists, numbered lists (decimal, alpha, roman)
  • Interactive checklists with strikethrough
  • Headings (H1-H6), blockquotes
  • Horizontal rules

Tables

  • Insert/delete rows and columns, merge cells
  • Drag-to-resize columns and rows
  • Header row styling
  • Row striping with custom colors
  • Right-click context menu for table actions

Media

  • Image upload with drag-drop and 8-point resize handles
  • Video embed with drag-drop and paste support
  • File attachments (downloadable files)
  • Link insertion with floating editor
  • Automatic link preview on hover
  • Rich embeds for external content (YouTube, etc.)

AI Features (Premium)

  • AI Chat assistant for content help
  • Smart autocomplete (AI-powered text suggestions)
  • Real-time grammar check and corrections
  • Text enhancement (improve, shorten, expand)
  • Tone adjustment (formal, casual, professional)
  • AI image generation from text prompts

Advanced

  • Slash commands (/ for quick formatting)
  • @Mentions with customizable user list
  • Inline comments with bubble UI and sidebar
  • Note panels (info, warning, error, success)
  • Code blocks with syntax highlighting
  • Interactive charts
  • Digital signature capture
  • Voice input / transcription
  • Export to PDF
  • HTML view toggle
  • Drag-and-drop block reordering
  • Markdown shortcuts

Dark Mode

The editor automatically detects your app's theme:

  • Checks for dark class on <html> or <body>
  • Falls back to prefers-color-scheme: dark system preference
  • Or set explicitly via the darkMode prop on EditorProvider

Preview Mode

Display saved editor content in read-only mode with interactive features:

<ConfigurableEditorWithAuth
  apiKey={apiKey}
  mode="preview"
  initialContent={savedHtml}
  onPreviewClick={() => setMode('edit')}
  containerClassName="my-preview-styles"
/>

API Reference

<EditorProvider>

Provides authentication and configuration context. Must wrap the editor component.

| Prop | Type | Required | Description | |------|------|----------|-------------| | children | ReactNode | Yes | Editor component to render | | defaultFontFamilies | string[] | No | Font family names for the font selector | | currentUser | CurrentUser | No | Current user for comments feature | | enableLinkPreview | boolean | No | Enable link preview on hover (default: true) | | apiKey | string | No | API key for link preview in read-only mode |

CurrentUser Type

interface CurrentUser {
  id: string;
  name: string;
  email: string;
  avatar?: string;
}

<ConfigurableEditorWithAuth>

The main editor component with authentication.

| Prop | Type | Required | Description | |------|------|----------|-------------| | apiKey | string | Yes | Your API key for authentication | | initialContent | string | No | Initial HTML content | | onChange | (html: string) => void | No | Content change callback | | defaultFontFamilies | string[] | No | Font names for the font selector | | mentionUserList | string[] | No | Usernames for @mention feature | | onAuthSuccess | () => void | No | Called when authentication succeeds | | onAuthError | (error: string) => void | No | Called when authentication fails | | customVerifyKey | (key: string) => Promise<ApiResponse> | No | Custom key verification function | | mode | "edit" \| "preview" | No | Editor mode (default: "edit") | | containerClassName | string | No | CSS class for the outermost editor container | | contentClassName | string | No | CSS class for the specific editor/preview content area | | editor | { maxHeight?: React.CSSProperties["maxHeight"] } | No | Editor container options (maxHeight) | | onPreviewClick | () => void | No | Click handler for preview mode | | enableReactNativeBridge | boolean | No | Enable React Native WebView bridge | | onEditorReady | () => void | No | Called when editor is fully loaded | | onFocus | () => void | No | Called on editor focus | | onBlur | () => void | No | Called on editor blur | | onHeightChange | (height: number) => void | No | Called when editor height changes | | toolbar | { mode?: "sticky" \| "static"; offset?: number; zIndex?: number } | No | Toolbar behavior config (default: { mode: "sticky", offset: 20, zIndex: 1000 }) |

Toolbar Configuration

Use the toolbar prop to control sticky/static toolbar behavior:

<ConfigurableEditorWithAuth
  apiKey="your-api-key"
  toolbar={{ mode: "sticky", offset: 64, zIndex: 1200 }}
/>

Modes:

  • mode: "sticky" -> toolbar detaches/sticks while scrolling and applies offset + zIndex
  • mode: "static" -> toolbar stays attached and ignores offset + zIndex even if provided

Defaults:

const defaultToolbar = {
  mode: "sticky",
  offset: 20,
  zIndex: 1000
};

In static mode, if you want only the editor content area to scroll, pass a maxHeight using editor:

<ConfigurableEditorWithAuth
  apiKey="your-api-key"
  toolbar={{ mode: "static" }}
  editor={{ maxHeight: 600 }}
/>

If maxHeight is not provided, the full page/container scrolls normally with the toolbar.

Examples

Basic Editor

import { ConfigurableEditorWithAuth, EditorProvider } from 'eddyter';
import 'eddyter/style.css';

export default function BasicEditor() {
  return (
    <EditorProvider>
      <ConfigurableEditorWithAuth
        apiKey="your-api-key"
        onAuthSuccess={() => console.log('Ready!')}
      />
    </EditorProvider>
  );
}

Editor with State Management

import { useState } from 'react';
import { ConfigurableEditorWithAuth, EditorProvider } from 'eddyter';
import 'eddyter/style.css';

export default function EditorWithState() {
  const [content, setContent] = useState('<p>Start writing...</p>');

  const handleSave = async () => {
    await fetch('/api/save', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ content })
    });
  };

  return (
    <div>
      <EditorProvider>
        <ConfigurableEditorWithAuth
          apiKey="your-api-key"
          initialContent={content}
          onChange={setContent}
        />
      </EditorProvider>
      <button onClick={handleSave}>Save</button>
    </div>
  );
}

Editor with Comments & Mentions

import { ConfigurableEditorWithAuth, EditorProvider } from 'eddyter';
import 'eddyter/style.css';

export default function EditorWithComments({ user }) {
  const currentUser = {
    id: user.id,
    name: user.name,
    email: user.email,
    avatar: user.avatarUrl
  };

  return (
    <EditorProvider currentUser={currentUser}>
      <ConfigurableEditorWithAuth
        apiKey="your-api-key"
        mentionUserList={['Alice', 'Bob', 'Charlie']}
      />
    </EditorProvider>
  );
}

Custom API Key Verification

import { ConfigurableEditorWithAuth, EditorProvider } from 'eddyter';
import 'eddyter/style.css';

export default function EditorWithCustomAuth() {
  const customVerifyKey = async (apiKey: string) => {
    try {
      const response = await fetch('/api/verify-key', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ apiKey })
      });
      const data = await response.json();
      return { success: data.valid, message: data.message || 'Verified' };
    } catch {
      return { success: false, message: 'Verification failed' };
    }
  };

  return (
    <EditorProvider>
      <ConfigurableEditorWithAuth
        apiKey="your-api-key"
        customVerifyKey={customVerifyKey}
      />
    </EditorProvider>
  );
}

Link Preview

The editor includes automatic link preview on hover.

  • Inside the editor: Works automatically after authentication
  • Read-only content: Pass apiKey to EditorProvider
  • Disable: Set enableLinkPreview={false} on EditorProvider
// Read-only content with link preview
<EditorProvider apiKey="your-api-key">
  <div dangerouslySetInnerHTML={{ __html: savedHtml }} />
</EditorProvider>

// Disable link preview
<EditorProvider enableLinkPreview={false}>
  {/* content */}
</EditorProvider>

React Native Integration

Use Eddyter in React Native via WebView by loading a deployed version of the editor.

npm install react-native-webview
import React, { useRef, useState, useCallback } from 'react';
import { View, ActivityIndicator, KeyboardAvoidingView, Platform } from 'react-native';
import { WebView, WebViewMessageEvent } from 'react-native-webview';

interface RichTextEditorProps {
  editorBaseUrl: string;
  apiKey: string;
  initialContent?: string;
  theme?: 'light' | 'dark';
  style?: object;
  onChange?: (content: string) => void;
  onReady?: () => void;
  onAuthSuccess?: () => void;
  onAuthError?: (error: string) => void;
}

export const RichTextEditor: React.FC<RichTextEditorProps> = ({
  editorBaseUrl,
  apiKey,
  initialContent,
  theme = 'light',
  style,
  onChange,
  onReady,
  onAuthSuccess,
  onAuthError,
}) => {
  const webViewRef = useRef<WebView>(null);
  const [isLoading, setIsLoading] = useState(true);

  const buildEditorUrl = () => {
    const baseUrl = editorBaseUrl.replace(/\/$/, '');
    const params = new URLSearchParams();
    if (apiKey) params.append('apiKey', apiKey);
    if (theme) params.append('theme', theme);
    return `${baseUrl}?${params.toString()}`;
  };

  const handleMessage = useCallback((event: WebViewMessageEvent) => {
    try {
      const message = JSON.parse(event.nativeEvent.data);
      switch (message.type) {
        case 'EDITOR_READY':
          setIsLoading(false);
          onReady?.();
          if (initialContent && webViewRef.current) {
            webViewRef.current.postMessage(
              JSON.stringify({ type: 'SET_CONTENT', payload: { content: initialContent } })
            );
          }
          break;
        case 'CONTENT_CHANGE':
          onChange?.(message.payload?.content || '');
          break;
        case 'AUTH_SUCCESS':
          onAuthSuccess?.();
          break;
        case 'AUTH_ERROR':
          onAuthError?.(message.payload?.error);
          break;
      }
    } catch (e) {
      console.warn('[RichTextEditor] Failed to parse message:', e);
    }
  }, [onChange, onReady, onAuthSuccess, onAuthError, initialContent]);

  return (
    <View style={[{ flex: 1 }, style]}>
      <WebView
        ref={webViewRef}
        source={{ uri: buildEditorUrl() }}
        style={{ flex: 1 }}
        onMessage={handleMessage}
        javaScriptEnabled={true}
        domStorageEnabled={true}
        keyboardDisplayRequiresUserAction={false}
      />
      {isLoading && (
        <View style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, justifyContent: 'center', alignItems: 'center' }}>
          <ActivityIndicator size="large" />
        </View>
      )}
    </View>
  );
};

Message Protocol

| Message Type | Direction | Description | |---|---|---| | EDITOR_READY | Editor → RN | Editor has finished loading | | CONTENT_CHANGE | Editor → RN | Content was modified ({ content: string }) | | AUTH_SUCCESS | Editor → RN | Authentication succeeded | | AUTH_ERROR | Editor → RN | Authentication failed ({ error: string }) | | SET_CONTENT | RN → Editor | Set editor content ({ content: string }) |

Exports

// Components
import {
  ConfigurableEditorWithAuth,  // Main editor with auth
  ConfigurableEditor,           // Editor without auth wrapper
  EditorProvider,               // Context provider
  LinkPreviewHover,             // Standalone link preview component
} from 'eddyter';

// Hooks & utilities
import {
  useEditor,                    // Access editor context
  useHtmlView,                  // Access HTML view state
  verifyApiKey,                 // Verify API key programmatically
  useReactNativeBridge,         // React Native bridge hook
  isReactNativeWebView,         // Check if running in RN WebView
} from 'eddyter';

// Config
import { defaultEditorConfig } from 'eddyter';

// Types
import type {
  CurrentUser,
  EditorConfigTypes,
  LinkPreviewHoverProps,
  ReactNativeBridgeConfig,
  ReactNativeMessage,
  ReactNativeMessageType,
} from 'eddyter';

License

Eddyter is proprietary software.

  • Free for evaluation and non-commercial use
  • Commercial use requires a paid license
  • SaaS, redistribution, and competing products are prohibited without permission

For commercial licensing, visit eddyter.com