# richtext-core-sdk

[![npm version](https://img.shields.io/npm/v/richtext-core-sdk.svg)](https://www.npmjs.com/package/richtext-core-sdk)
[![npm downloads](https://img.shields.io/npm/dw/richtext-core-sdk.svg)](https://www.npmjs.com/package/richtext-core-sdk)
[![Bundle Size](https://img.shields.io/bundlephobia/minzip/richtext-core-sdk)](https://bundlephobia.com/package/richtext-core-sdk)

Plug and Play AI Rich Text Editor for **any** website, blog, CRM, ERP, or web app — built on [Lexical](https://lexical.dev/) with dark mode support and API key authentication. No React knowledge required.

This is the **framework-agnostic SDK** for the Eddyter editor. It wraps the official React package [`eddyter`](https://www.npmjs.com/package/eddyter) and exposes a single `init()` function so you can mount the editor into any HTML element from plain JavaScript, Vue, Svelte, Angular, Laravel/Blade, WordPress, or any other stack.

![Eddyter Editor](https://dsckj64r0usqg.cloudfront.net/assets/seo-screen.png)

## Resources

- [Docs](https://eddyter.com/docs) — Full API reference and integration guides
- [What is Eddyter? Why Developers Are Switching to This AI Editor (2026)](https://youtu.be/oNHBa-DImZc) — YouTube
- [Integrate Eddyter in 30 Minutes Using AI Tools Cursor, Claude, Lovable](https://youtu.be/5lTjRFjUWgs) — YouTube

## Installation

```bash
npm install richtext-core-sdk
# or
yarn add richtext-core-sdk
# or
pnpm add richtext-core-sdk
```

Or include it directly via a CDN as a classic script:

```html
<script src="https://unpkg.com/richtext-core-sdk/dist/editor.iife.js"></script>
```

### Compatibility

| Requirement | Version |
|-------------|---------|
| Node.js | 16+ |
| Browsers | Evergreen (Chrome, Edge, Firefox, Safari) |
| Frameworks | Any — React, Vue, Svelte, Angular, vanilla JS, server-rendered HTML |

> Internally the SDK ships React + ReactDOM bundled with the editor, so your host app does **not** need to install or configure React.

## Quick Start

### 1. Import styles

```js
import 'richtext-core-sdk/style.css';
```

> **Important:** The stylesheet is required for tables, toolbars, and all editor components to render correctly. When the SDK is loaded as a classic script (`editor.iife.js`), the stylesheet is injected automatically by the IIFE bootstrap.

### 2. Get your API key

1. Create an account at [eddyter.com](https://www.eddyter.com/)
2. Navigate to [License Keys](https://www.eddyter.com/user/license-key) in your dashboard
3. Copy your API key

### 3. Add the editor

```html
<div id="editor"></div>
```

```js
import { init } from 'richtext-core-sdk';
import 'richtext-core-sdk/style.css';

const apiKey = 'YOUR_API_KEY';

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

const instance = init({
  container: '#editor',
  apiKey,
  user: currentUser,
  initialContent: '<p>Start writing...</p>',
  mentionUserList: ['Alice', 'Bob', 'Charlie'],
  onChange: (html) => console.log('Content:', html),
  onReady: () => console.log('Editor ready!'),
  onAuthSuccess: () => console.log('Auth succeeded'),
  onAuthError: (error) => console.error('Auth failed:', error),
});
```

When loaded via `<script>`, the same API is available on `window.Eddyter`:

```html
<div id="editor"></div>

<script src="https://unpkg.com/richtext-core-sdk/dist/editor.iife.js"></script>
<script>
  const instance = Eddyter.init({
    container: '#editor',
    apiKey: 'YOUR_API_KEY',
    onChange: (html) => console.log(html),
  });
</script>
```

## 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` option, and toggle it at runtime without remounting:

```js
instance.update({ darkMode: true });
```

### Preview Mode

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

```js
init({
  container: '#preview',
  apiKey: 'YOUR_API_KEY',
  mode: 'preview',
  initialContent: savedHtml,
  onPreviewClick: () => switchToEditMode(),
  containerClass: 'my-preview-styles',
});
```

## API Reference

### `init(options)` → `EddyterInstance`

Mounts the editor into a DOM container and returns a small instance handle. Calling `init()` twice on the same container returns the existing instance instead of remounting.

#### `EddyterInitOptions`

| Option | Type | Required | Description |
|--------|------|----------|-------------|
| `container` | `string \| HTMLElement` | Yes | CSS selector or DOM element to mount into |
| `apiKey` | `string` | Yes | Your Eddyter license key |
| `user` | `EddyterCurrentUser` | No | Current user for comments / mentions. Defaults to `{ id: 'anonymous', name: 'Anonymous' }` |
| `initialContent` | `string` | No | Initial HTML rendered in the editor |
| `mode` | `'edit' \| 'preview'` | No | Editor mode (default: `'edit'`) |
| `darkMode` | `boolean` | No | `true`/`false`. Omit to auto-detect host's `.dark` class |
| `customVerifyKey` | `(key: string) => Promise<EddyterApiResponse>` | No | Use your own backend to validate the API key |
| `mentionUserList` | `string[]` | No | Names that appear in `@mention` suggestions |
| `defaultFontFamilies` | `string[]` | No | Font family names for the font selector |
| `class` | `string` | No | CSS class applied to the outermost editor wrapper |
| `containerClass` | `string` | No | CSS class applied to the **preview** container (only used when `mode: 'preview'`) |
| `contentClass` | `string` | No | CSS class applied to the editable **content area** |
| `floatingToolbarClass` | `string` | No | CSS class applied to the floating toolbar and its portal container |
| `style` | `React.CSSProperties` | No | Inline style object applied to the wrapper |
| `toolbar` | `EddyterToolbarConfig` | No | Toolbar behavior (default: `{ mode: 'sticky', offset: 20, zIndex: 1000 }`) |
| `editor` | `EddyterEditorOptions` | No | Editor container options (`maxHeight`) |
| `enableReactNativeBridge` | `boolean` | No | Force-enable RN WebView bridge messaging (auto-detected in a WebView context) |
| `onChange` | `(html: string) => void` | No | Editor content changes (debounced) |
| `onReady` | `() => void` | No | Editor finished mounting and authenticated |
| `onAuthSuccess` | `() => void` | No | API key validated successfully |
| `onAuthError` | `(error: string) => void` | No | API key validation failed |
| `onFocus` | `() => void` | No | Editor gained focus (React Native bridge) |
| `onBlur` | `() => void` | No | Editor lost focus (React Native bridge) |
| `onHeightChange` | `(height: number) => void` | No | Editor content height changes (React Native bridge) |
| `onPreviewClick` | `() => void` | No | User clicks anywhere inside the preview (e.g. to open edit mode) |

#### Supporting types

```ts
interface EddyterCurrentUser {
  id: string;
  name: string;
  email?: string;
  avatar?: string;
}

interface EddyterApiResponse {
  success: boolean;
  message: string;
  data?: unknown;
}

interface EddyterToolbarConfig {
  mode?: 'sticky' | 'static';
  offset?: number;
  zIndex?: number;
}

interface EddyterEditorOptions {
  maxHeight?: string | number;
}
```

### `EddyterInstance`

```ts
interface EddyterInstance {
  destroy(): void;
  update(partial: EddyterUpdatableOptions): void;
}
```

#### `instance.update(partial)`

Updates the editor **without remounting**. Useful for theme toggles, toolbar mode changes, and class swaps.

Supported fields:

```
mode, darkMode, class, containerClass, contentClass,
floatingToolbarClass, style, toolbar, editor,
defaultFontFamilies, mentionUserList, enableReactNativeBridge
```

```js
instance.update({ darkMode: true });
instance.update({ toolbar: { mode: 'static' }, editor: { maxHeight: 480 } });
```

> `apiKey`, `container`, `user`, `initialContent`, and `customVerifyKey` require a fresh `init()` — they drive the auth/provider lifecycle and should not be hot-swapped.

#### `instance.destroy()`

Unmounts the editor and clears the singleton stored on the container element. Call this when removing the editor from the DOM (modal close, SPA route change, etc.).

```js
instance.destroy();
```

### Toolbar Configuration

Use the `toolbar` option to control sticky/static toolbar behavior:

```js
init({
  container: '#editor',
  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:

```ts
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`:

```js
init({
  container: '#editor',
  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

```js
import { init } from 'richtext-core-sdk';
import 'richtext-core-sdk/style.css';

init({
  container: '#editor',
  apiKey: 'your-api-key',
  onReady: () => console.log('Ready!'),
});
```

### Editor with State Management

Since the SDK is framework-agnostic, you wire state via the `onChange` callback:

```js
import { init } from 'richtext-core-sdk';
import 'richtext-core-sdk/style.css';

let content = '<p>Start writing...</p>';

const instance = init({
  container: '#editor',
  apiKey: 'your-api-key',
  initialContent: content,
  onChange: (html) => {
    content = html;
  },
});

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

### Editor with Comments & Mentions

```js
init({
  container: '#editor',
  apiKey: 'your-api-key',
  user: {
    id: currentUser.id,
    name: currentUser.name,
    email: currentUser.email,
    avatar: currentUser.avatarUrl,
  },
  mentionUserList: ['Alice', 'Bob', 'Charlie'],
});
```

### Custom API Key Verification

```js
init({
  container: '#editor',
  apiKey: 'your-api-key',
  customVerifyKey: async (apiKey) => {
    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' };
    }
  },
});
```

### Theme Toggle Without Remount

```js
const instance = init({
  container: '#editor',
  apiKey: 'your-api-key',
});

let isDark = false;
document.getElementById('theme-toggle')?.addEventListener('click', () => {
  isDark = !isDark;
  instance.update({ darkMode: isDark });
});
```

### Static Toolbar with Scrollable Content

```js
init({
  container: '#editor',
  apiKey: 'your-api-key',
  toolbar: { mode: 'static' },
  editor: { maxHeight: '420px' },
});
```

### Multiple Editors on One Page

Each container gets its own instance. Call `init()` once per container and `destroy()` when removing it.

```js
const a = init({ container: '#editor-a', apiKey: 'your-api-key' });
const b = init({ container: '#editor-b', apiKey: 'your-api-key' });

a.destroy();
b.destroy();
```

## Link Preview

The editor includes automatic link preview on hover. It works automatically inside the editor after authentication and inside the preview when `mode: 'preview'` is set with a valid `apiKey`.

## Framework Integration

The SDK ships official thin wrappers for popular frameworks. If you prefer a more idiomatic API, use the framework package — internally each one calls `init()` from this SDK.

- **Vue** — [`richtext-core-vue`](https://www.npmjs.com/package/richtext-core-vue)
- **Svelte** — [`richtext-core-svelte`](https://www.npmjs.com/package/richtext-core-svelte)
- **Angular** — [`richtext-core-angular`](https://www.npmjs.com/package/richtext-core-angular)
- **React** — use the original [`eddyter`](https://www.npmjs.com/package/eddyter) package directly

For server-rendered apps (Laravel/Blade, Django, Rails, WordPress, etc.) load the IIFE bundle via `<script>` and call `Eddyter.init(...)` from a small inline script.

## React Native Integration

Use Eddyter in React Native via WebView by loading a deployed version of the editor. The bridge messaging is automatically enabled when running inside a WebView, or you can force it on via the `enableReactNativeBridge` option on the host page.

```bash
npm install react-native-webview
```

```tsx
import React, { useRef, useState, useCallback } from 'react';
import { View, ActivityIndicator } 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

```ts
// Entrypoint
import { init } from 'richtext-core-sdk';

// Types
import type {
  EddyterInitOptions,
  EddyterInstance,
  EddyterUpdatableOptions,
  EddyterCurrentUser,
  EddyterToolbarConfig,
  EddyterEditorOptions,
  EddyterApiResponse,
} from 'richtext-core-sdk';

// Styles (one-time import at app entry)
import 'richtext-core-sdk/style.css';
```

When loaded as a classic script the SDK is available on `window.Eddyter`:

```js
window.Eddyter.init({ /* options */ });
```

## Troubleshooting

| Symptom | Fix |
|---------|-----|
| Editor renders but toolbars look broken | The stylesheet is missing. Import `richtext-core-sdk/style.css` in the entry that builds production. |
| `Invalid container` thrown | The selector did not resolve, or you passed something that isn't an `HTMLElement`. |
| Theme does not switch | Use `instance.update({ darkMode })`; do not remount. |
| API key errors | Confirm the key in your env vars and check the network tab for the verification request. |
| Editor mounts twice in dev (StrictMode-like behavior) | `init()` is idempotent per container — it returns the existing instance if one exists. |

## Migration

If you are upgrading from an earlier release, some legacy options were removed because they targeted React props that never existed:

| Old option | New option | Status |
|------------|------------|--------|
| `previewClass` | `containerClass` | Removed — old value was a no-op |
| `editorClass` | `contentClass` | Removed — old value was a no-op |
| `previewClassName` | `containerClass` | Removed (never wired up) |
| `editorClassName` | `contentClass` | Removed (never wired up) |
| — | `floatingToolbarClass` | **New** — style the floating toolbar / portal |

```diff
- init({ container: '#editor', apiKey, previewClass: 'p', editorClass: 'c' });
+ init({ container: '#editor', apiKey, containerClass: 'p', contentClass: 'c' });
```

## 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](https://www.eddyter.com/)
