@remyxjs/react 1.0.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +2158 -0
  2. package/create/index.js +367 -0
  3. package/create/templates/jsx/index.html +12 -0
  4. package/create/templates/jsx/src/App.jsx +25 -0
  5. package/create/templates/jsx/src/main.jsx +9 -0
  6. package/create/templates/jsx/vite.config.js +6 -0
  7. package/create/templates/typescript/index.html +12 -0
  8. package/create/templates/typescript/src/App.tsx +25 -0
  9. package/create/templates/typescript/src/main.tsx +9 -0
  10. package/create/templates/typescript/src/vite-env.d.ts +1 -0
  11. package/create/templates/typescript/tsconfig.json +21 -0
  12. package/create/templates/typescript/vite.config.ts +6 -0
  13. package/dist/AttachmentModal--6T-vYJt.js +21 -0
  14. package/dist/AttachmentModal--6T-vYJt.js.map +1 -0
  15. package/dist/AttachmentModal-D-uxbXvb.cjs +2 -0
  16. package/dist/AttachmentModal-D-uxbXvb.cjs.map +1 -0
  17. package/dist/CommandPalette-DXWyTGOU.js +19 -0
  18. package/dist/CommandPalette-DXWyTGOU.js.map +1 -0
  19. package/dist/CommandPalette-t9J8JGNJ.cjs +2 -0
  20. package/dist/CommandPalette-t9J8JGNJ.cjs.map +1 -0
  21. package/dist/ContextMenu-B4K3Zbfp.cjs +2 -0
  22. package/dist/ContextMenu-B4K3Zbfp.cjs.map +1 -0
  23. package/dist/ContextMenu-D8wNSMc3.js +2 -0
  24. package/dist/ContextMenu-D8wNSMc3.js.map +1 -0
  25. package/dist/EmbedModal-Bh2Tcow9.cjs +2 -0
  26. package/dist/EmbedModal-Bh2Tcow9.cjs.map +1 -0
  27. package/dist/EmbedModal-cxE4maD2.js +11 -0
  28. package/dist/EmbedModal-cxE4maD2.js.map +1 -0
  29. package/dist/ExportModal-Cf1uE4r_.js +13 -0
  30. package/dist/ExportModal-Cf1uE4r_.js.map +1 -0
  31. package/dist/ExportModal-DwQVsrZE.cjs +2 -0
  32. package/dist/ExportModal-DwQVsrZE.cjs.map +1 -0
  33. package/dist/FindReplaceModal-DYL_2z8U.cjs +2 -0
  34. package/dist/FindReplaceModal-DYL_2z8U.cjs.map +1 -0
  35. package/dist/FindReplaceModal-Dgt_MrWb.js +17 -0
  36. package/dist/FindReplaceModal-Dgt_MrWb.js.map +1 -0
  37. package/dist/ImageModal-D39ywxqI.cjs +2 -0
  38. package/dist/ImageModal-D39ywxqI.cjs.map +1 -0
  39. package/dist/ImageModal-DqScpPrc.js +23 -0
  40. package/dist/ImageModal-DqScpPrc.js.map +1 -0
  41. package/dist/ImportDocumentModal-BKqMxO3z.js +22 -0
  42. package/dist/ImportDocumentModal-BKqMxO3z.js.map +1 -0
  43. package/dist/ImportDocumentModal-Bev9hp_J.cjs +2 -0
  44. package/dist/ImportDocumentModal-Bev9hp_J.cjs.map +1 -0
  45. package/dist/LinkModal-B-igSa-g.cjs +2 -0
  46. package/dist/LinkModal-B-igSa-g.cjs.map +1 -0
  47. package/dist/LinkModal-k9IeDtAb.js +15 -0
  48. package/dist/LinkModal-k9IeDtAb.js.map +1 -0
  49. package/dist/MenuBar-B-ZAX9rH.cjs +2 -0
  50. package/dist/MenuBar-B-ZAX9rH.cjs.map +1 -0
  51. package/dist/MenuBar-DWxJNHmb.js +12 -0
  52. package/dist/MenuBar-DWxJNHmb.js.map +1 -0
  53. package/dist/ModalOverlay-BDsGgv3_.cjs +2 -0
  54. package/dist/ModalOverlay-BDsGgv3_.cjs.map +1 -0
  55. package/dist/ModalOverlay-CLvRNHmp.js +6 -0
  56. package/dist/ModalOverlay-CLvRNHmp.js.map +1 -0
  57. package/dist/SourceModal-BNI_i4iW.cjs +2 -0
  58. package/dist/SourceModal-BNI_i4iW.cjs.map +1 -0
  59. package/dist/SourceModal-MdTGK3Uf.js +12 -0
  60. package/dist/SourceModal-MdTGK3Uf.js.map +1 -0
  61. package/dist/TablePickerModal-DYODWEA1.js +17 -0
  62. package/dist/TablePickerModal-DYODWEA1.js.map +1 -0
  63. package/dist/TablePickerModal-Do1QyoyM.cjs +2 -0
  64. package/dist/TablePickerModal-Do1QyoyM.cjs.map +1 -0
  65. package/dist/index-C720tbJA.js +359 -0
  66. package/dist/index-C720tbJA.js.map +1 -0
  67. package/dist/index-Dc63uIP0.cjs +2 -0
  68. package/dist/index-Dc63uIP0.cjs.map +1 -0
  69. package/dist/remyx-react.cjs +2 -0
  70. package/dist/remyx-react.cjs.map +1 -0
  71. package/dist/remyx-react.css +1 -0
  72. package/dist/remyx-react.js +2 -0
  73. package/dist/remyx-react.js.map +1 -0
  74. package/package.json +69 -0
package/README.md ADDED
@@ -0,0 +1,2158 @@
1
+ ![Remyx Editor](../docs/screenshots/Remyx-Logo.svg)
2
+
3
+ # @remyxjs/react
4
+
5
+ A feature-rich WYSIWYG editor for React, built on the framework-agnostic [`@remyxjs/core`](../remyx-core/) engine. Configurable toolbar, menu bar, markdown support, theming, file uploads, and a plugin system.
6
+
7
+ ## Packages
8
+
9
+ | Package | Version | Description |
10
+ | --- | --- | --- |
11
+ | [`@remyxjs/core`](../remyx-core/) | 1.0.0-beta | Framework-agnostic engine, commands, plugins, utilities, and CSS themes |
12
+ | [`@remyxjs/react`](./) | 1.0.0-beta | React components, hooks, and TypeScript declarations |
13
+
14
+ Use `@remyxjs/core` directly if building a wrapper for another framework (Vue, Svelte, Angular, vanilla JS).
15
+
16
+ ## Table of Contents
17
+
18
+ - [Installation](#installation)
19
+ - [Quick Start](#quick-start)
20
+ - [Features](#features)
21
+ - [Props](#props)
22
+ - [Config File](#config-file)
23
+ - [Toolbar](#toolbar)
24
+ - [Custom Toolbar](#custom-toolbar-via-props)
25
+ - [Presets](#toolbar-presets)
26
+ - [Helper Functions](#toolbar-helper-functions)
27
+ - [Available Items](#available-toolbar-items)
28
+ - [Menu Bar](#menu-bar)
29
+ - [Multiple Editors](#multiple-editors)
30
+ - [Fonts](#font-configuration)
31
+ - [Status Bar](#status-bar)
32
+ - [Theming](#theming)
33
+ - [Custom Themes](#custom-themes)
34
+ - [Theme Presets](#theme-presets)
35
+ - [Theme Variables](#available-theme-variables)
36
+ - [Per-Item Toolbar Theming](#per-item-toolbar-theming)
37
+ - [Paste Handling](#paste--auto-conversion)
38
+ - [File Uploads](#file-uploads)
39
+ - [Output Formats](#output-formats)
40
+ - [Attaching to Existing Elements](#attaching-to-existing-elements)
41
+ - [Plugins](#plugins)
42
+ - [Read-Only Mode](#read-only-mode)
43
+ - [Error Handling](#error-handling)
44
+ - [Engine Access](#engine-access)
45
+ - [Import & Export Documents](#import--export-documents)
46
+ - [Sanitization](#sanitization)
47
+ - [Keyboard Shortcuts](#keyboard-shortcuts)
48
+ - [Heading Level Offset](#heading-level-offset)
49
+ - [Floating Toolbar & Context Menu](#floating-toolbar--context-menu)
50
+ - [Autosave](#autosave)
51
+ - [UX/UI Features](#uxui-features)
52
+ - [Collaboration](#collaboration)
53
+ - [Form Integration](#form-integration)
54
+ - [Exports](#exports)
55
+ - [TypeScript](#typescript)
56
+ - [Using `@remyxjs/core` Directly](#using-remyxcore-directly)
57
+
58
+ ## Installation
59
+
60
+ ```bash
61
+ npm install @remyxjs/core @remyxjs/react
62
+ ```
63
+
64
+ Import both stylesheets in your app entry point:
65
+
66
+ ```js
67
+ import '@remyxjs/core/style.css'; // theme variables, light/dark themes
68
+ import '@remyxjs/react/style.css'; // component styles (toolbar, modals, etc.)
69
+ ```
70
+
71
+ ## Quick Start
72
+
73
+ ```jsx
74
+ import { useState } from 'react';
75
+ import { RemyxEditor } from '@remyxjs/react';
76
+ import '@remyxjs/core/style.css';
77
+ import '@remyxjs/react/style.css';
78
+
79
+ function App() {
80
+ const [content, setContent] = useState('');
81
+
82
+ return (
83
+ <RemyxEditor
84
+ value={content}
85
+ onChange={setContent}
86
+ placeholder="Start typing..."
87
+ height={400}
88
+ />
89
+ );
90
+ }
91
+ ```
92
+
93
+ ### Uncontrolled Mode
94
+
95
+ ```jsx
96
+ <RemyxEditor
97
+ defaultValue="<p>Initial content</p>"
98
+ onChange={(html) => console.log(html)}
99
+ />
100
+ ```
101
+
102
+ ### Markdown Mode
103
+
104
+ ```jsx
105
+ const [markdown, setMarkdown] = useState('# Hello\n\nStart typing...');
106
+
107
+ <RemyxEditor
108
+ value={markdown}
109
+ onChange={setMarkdown}
110
+ outputFormat="markdown"
111
+ />
112
+ ```
113
+
114
+ ### With Upload Handler
115
+
116
+ ```jsx
117
+ <RemyxEditor
118
+ value={content}
119
+ onChange={setContent}
120
+ uploadHandler={async (file) => {
121
+ const formData = new FormData();
122
+ formData.append('file', file);
123
+ const res = await fetch('/api/upload', { method: 'POST', body: formData });
124
+ const { url } = await res.json();
125
+ return url;
126
+ }}
127
+ />
128
+ ```
129
+
130
+ ### Dark Theme with Google Fonts
131
+
132
+ ```jsx
133
+ <RemyxEditor
134
+ theme="dark"
135
+ googleFonts={['Inter', 'Fira Code', 'Merriweather']}
136
+ height={500}
137
+ />
138
+ ```
139
+
140
+ ## Features
141
+
142
+ - **Rich text editing** — bold, italic, underline, strikethrough, subscript, superscript, headings, lists, blockquotes, code blocks, tables (with sorting, filtering, formulas, cell formatting, resize handles, and clipboard interop), and horizontal rules
143
+ - **Toolbar** — fully configurable with presets, helper functions, and per-item theming
144
+ - **Menu bar** — application-style menus (File, Edit, View, Insert, Format) with submenus and auto-deduplication
145
+ - **Markdown** — bidirectional HTML/Markdown conversion, toggle between modes, and auto-detection on paste
146
+ - **Theming** — light/dark mode, 4 built-in presets (Ocean, Forest, Sunset, Rose), full CSS variable customization
147
+ - **File uploads** — images and attachments via drag-and-drop, paste, or toolbar with pluggable upload handlers (S3, R2, GCS, custom)
148
+ - **Fonts** — custom font lists, Google Fonts auto-loading, and helper functions
149
+ - **Code block syntax highlighting** — 11 languages with auto-detection, theme-aware colors, language selector dropdown, line numbers toggle, copy-to-clipboard, inline code highlighting, and extensible language registry
150
+ - **Enhanced tables** — sortable columns, multi-column sort, filterable rows, inline formulas, cell formatting, column/row resize, sticky headers, and Excel/Sheets clipboard interop via `TablePlugin`
151
+ - **Template system** — `{{merge_tag}}` chips, `{{#if}}`/`{{#each}}` blocks, live preview, 5 pre-built templates, custom template library via `TemplatePlugin`
152
+ - **Keyboard-first editing** — Vim (normal/insert/visual), Emacs keybindings, custom key map, multi-cursor (`Cmd+D`), smart auto-pairing, jump-to-heading via `KeyboardPlugin`
153
+ - **Drag-and-drop content** — Drop zones, cross-editor drag, file/image/rich-text drops, block reorder with ghost preview via `DragDropPlugin`
154
+ - **Advanced link management** — Link previews, broken link detection, auto-linking, click analytics, bookmark anchors via `LinkPlugin`
155
+ - **Callouts & alerts** — 7 built-in callout types with custom type registration, collapsible toggle, nested content, and GitHub-flavored alert syntax auto-conversion via `CalloutPlugin`
156
+ - **Comments & annotations** — Inline comment threads via `CommentsPlugin`, `CommentsPanel` sidebar with thread cards/replies/actions, `useComments` hook for reactive state, @mention parsing, resolved/unresolved state, comment-only mode, import/export
157
+ - **Real-time collaboration** — CRDT-based co-editing with live cursors, presence indicators, offline-first sync, configurable transport (WebSocket, WebRTC, or custom) via `CollaborationPlugin`, `useCollaboration` hook, `CollaborationBar` component
158
+ - **Plugins** — `createPlugin()` API with hooks for commands, toolbar items, status bar items, and context menus
159
+ - **Config file** — centralized `defineConfig()` with named editor configurations and provider-based sharing
160
+ - **Block-based editing** — block-level toolbar with type conversion, drag-to-reorder, collapsible sections, block grouping, `BlockTemplatePlugin` with built-in templates
161
+ - **Mobile & touch** — touch floating toolbar, swipe indent/outdent, long-press context menu with haptic feedback, pinch-to-zoom, responsive toolbar overflow, virtual keyboard-aware layout
162
+ - **Multi-instance** — unlimited editors per page with full isolation (state, events, DOM, modals), `EditorBus` for inter-editor communication, `SharedResources` for memory-efficient shared schemas and icons
163
+ - **Import/Export** — PDF, DOCX, Markdown, CSV, and HTML
164
+ - **Paste cleaning** — intelligent cleanup from Word, Google Docs, LibreOffice, Pages, and raw markdown
165
+ - **Command palette** — searchable overlay with all editor commands, opened via `Mod+Shift+P` or toolbar button
166
+ - **Keyboard shortcuts** — customizable with sensible defaults
167
+ - **Accessibility** — semantic HTML, ARIA attributes, keyboard navigation, and focus management
168
+
169
+ ## Props
170
+
171
+ | Prop | Type | Default | Description |
172
+ | --- | --- | --- | --- |
173
+ | `config` | `string` | — | Named editor config from `defineConfig()` |
174
+ | `value` | `string` | — | Controlled content (HTML or Markdown) |
175
+ | `defaultValue` | `string` | — | Initial content for uncontrolled mode |
176
+ | `onChange` | `(content: string) => void` | — | Called when content changes |
177
+ | `outputFormat` | `'html' \| 'markdown'` | `'html'` | Format passed to `onChange` |
178
+ | `toolbar` | `string[][]` | Full toolbar | Custom toolbar layout |
179
+ | `menuBar` | `boolean \| MenuBarConfig[]` | — | Enable menu bar |
180
+ | `theme` | `'light' \| 'dark' \| 'ocean' \| 'forest' \| 'sunset' \| 'rose'` | `'light'` | Editor theme |
181
+ | `placeholder` | `string` | `''` | Placeholder text |
182
+ | `height` | `number` | `300` | Editor height in px |
183
+ | `minHeight` | `number` | — | Minimum height |
184
+ | `maxHeight` | `number` | — | Maximum height (scrolls) |
185
+ | `readOnly` | `boolean` | `false` | Disable editing |
186
+ | `fonts` | `string[]` | Built-in list | Custom font families |
187
+ | `googleFonts` | `string[]` | — | Google Font families to auto-load |
188
+ | `statusBar` | `'bottom' \| 'top' \| 'popup' \| false` | `'bottom'` | Word/character count position |
189
+ | `customTheme` | `object` | — | CSS variable overrides |
190
+ | `toolbarItemTheme` | `object` | — | Per-item toolbar styling |
191
+ | `floatingToolbar` | `boolean` | `true` | Show toolbar on text selection |
192
+ | `contextMenu` | `boolean` | `true` | Show right-click context menu |
193
+ | `commandPalette` | `boolean` | `true` | Enable command palette (Mod+Shift+P or toolbar button) |
194
+ | `autosave` | `boolean \| AutosaveConfig` | `false` | Enable autosave with optional config (storage provider, interval, key) |
195
+ | `emptyState` | `boolean \| ReactNode` | `false` | Show empty state when editor has no content (true for default, or custom React node) |
196
+ | `breadcrumb` | `boolean` | `false` | Show breadcrumb bar with DOM path to current selection |
197
+ | `minimap` | `boolean` | `false` | Show minimap preview on right edge |
198
+ | `splitViewFormat` | `'html' \| 'markdown'` | `'html'` | Format for split view preview pane |
199
+ | `customizableToolbar` | `boolean` | `false` | Enable drag-and-drop toolbar button rearrangement |
200
+ | `onToolbarChange` | `(order: string[]) => void` | — | Called when toolbar order changes via drag |
201
+ | `plugins` | `Plugin[]` | — | Custom plugins |
202
+ | `uploadHandler` | `(file: File) => Promise<string>` | — | File upload handler |
203
+ | `shortcuts` | `object` | — | Keyboard shortcut overrides |
204
+ | `sanitize` | `object` | — | HTML sanitization options |
205
+ | `attachTo` | `React.RefObject` | — | Attach to an existing element |
206
+ | `onReady` | `(engine) => void` | — | Called when editor initializes |
207
+ | `onFocus` | `() => void` | — | Called on focus |
208
+ | `onBlur` | `() => void` | — | Called on blur |
209
+ | `className` | `string` | `''` | CSS class for wrapper |
210
+ | `style` | `object` | — | Inline styles for wrapper |
211
+
212
+ ## Config File
213
+
214
+ Define shared defaults and named editor configurations in a centralized config file instead of repeating props.
215
+
216
+ **`remyx.config.js`**
217
+
218
+ ```js
219
+ import { defineConfig } from '@remyxjs/react';
220
+
221
+ export default defineConfig({
222
+ theme: 'dark',
223
+ placeholder: 'Start writing...',
224
+ height: 400,
225
+
226
+ editors: {
227
+ minimal: {
228
+ toolbar: [['bold', 'italic', 'underline'], ['link']],
229
+ floatingToolbar: false,
230
+ height: 200,
231
+ },
232
+ comments: {
233
+ toolbar: [['bold', 'italic', 'strikethrough'], ['orderedList', 'unorderedList'], ['link']],
234
+ statusBar: false,
235
+ height: 150,
236
+ placeholder: 'Write a comment...',
237
+ },
238
+ },
239
+ });
240
+ ```
241
+
242
+ **Usage**
243
+
244
+ ```jsx
245
+ import { RemyxEditor, RemyxConfigProvider } from '@remyxjs/react';
246
+ import config from './remyx.config.js';
247
+
248
+ function App() {
249
+ return (
250
+ <RemyxConfigProvider config={config}>
251
+ <RemyxEditor /> {/* default config */}
252
+ <RemyxEditor config="minimal" /> {/* named config */}
253
+ <RemyxEditor config="minimal" theme="light" /> {/* prop overrides config */}
254
+ </RemyxConfigProvider>
255
+ );
256
+ }
257
+ ```
258
+
259
+ ### Config Resolution Priority
260
+
261
+ Values merge with this priority (highest first):
262
+
263
+ 1. **Component props** — always win
264
+ 2. **Named editor config** — `editors.minimal`, `editors.comments`, etc.
265
+ 3. **Default config** — top-level keys in the config file
266
+ 4. **Built-in defaults** — `theme: 'light'`, `height: 300`, etc.
267
+
268
+ Editors work identically without a `RemyxConfigProvider` — the provider is optional.
269
+
270
+ ### External Configuration (JSON/YAML URL)
271
+
272
+ Load the entire editor config from a remote JSON or YAML file — no code changes needed to update the editor:
273
+
274
+ ```jsx
275
+ import { RemyxEditorFromConfig } from '@remyxjs/react';
276
+
277
+ function App() {
278
+ return (
279
+ <RemyxEditorFromConfig
280
+ url="/editor-config.json"
281
+ env="production"
282
+ value={content}
283
+ onChange={setContent}
284
+ loadingFallback={<div>Loading editor...</div>}
285
+ errorFallback={({ error, reload }) => (
286
+ <div>
287
+ <p>Config failed to load: {error.message}</p>
288
+ <button onClick={reload}>Retry</button>
289
+ </div>
290
+ )}
291
+ />
292
+ );
293
+ }
294
+ ```
295
+
296
+ | Prop | Type | Description |
297
+ | --- | --- | --- |
298
+ | `url` | `string` | URL to a JSON or YAML config file |
299
+ | `env` | `string` | Environment name for config merging (e.g., `'production'`) |
300
+ | `fetchHeaders` | `Record<string, string>` | Custom headers for authenticated config endpoints |
301
+ | `pollInterval` | `number` | Auto-reload interval in ms (0 = disabled) |
302
+ | `onConfigLoad` | `(config) => void` | Callback when config loads successfully |
303
+ | `onConfigError` | `(error) => void` | Callback on load error |
304
+ | `loadingFallback` | `ReactNode` | UI shown while loading (default: null) |
305
+ | `errorFallback` | `ReactNode \| ({ error, reload }) => ReactNode` | UI shown on error (default: retry button) |
306
+ | `...rest` | — | All other props forwarded to `<RemyxEditor />` (overrides loaded config) |
307
+
308
+ #### useExternalConfig hook
309
+
310
+ For more control, use the hook directly:
311
+
312
+ ```jsx
313
+ import { useExternalConfig, RemyxEditor } from '@remyxjs/react';
314
+
315
+ function MyEditor() {
316
+ const { config, loading, error, reload } = useExternalConfig('/editor-config.yaml', {
317
+ env: process.env.NODE_ENV,
318
+ pollInterval: 60000, // Auto-reload every 60s
319
+ onLoad: (cfg) => console.log('Config loaded:', cfg),
320
+ onError: (err) => console.error('Config error:', err),
321
+ });
322
+
323
+ if (loading) return <div>Loading...</div>;
324
+ if (error) return <button onClick={reload}>Retry</button>;
325
+
326
+ return <RemyxEditor {...config} value={content} onChange={setContent} />;
327
+ }
328
+ ```
329
+
330
+ | Return value | Type | Description |
331
+ | --- | --- | --- |
332
+ | `config` | `object \| null` | The loaded configuration, or null before first load |
333
+ | `loading` | `boolean` | True while fetching |
334
+ | `error` | `Error \| null` | Fetch or parse error, or null |
335
+ | `reload` | `() => Promise<void>` | Re-fetch the config (cancels any in-flight request) |
336
+
337
+ ## Toolbar
338
+
339
+ ### Default Toolbar
340
+
341
+ ```js
342
+ [
343
+ ['undo', 'redo'],
344
+ ['headings', 'fontFamily', 'fontSize'],
345
+ ['bold', 'italic', 'underline', 'strikethrough'],
346
+ ['foreColor', 'backColor'],
347
+ ['alignLeft', 'alignCenter', 'alignRight', 'alignJustify'],
348
+ ['orderedList', 'unorderedList', 'taskList'],
349
+ ['outdent', 'indent'],
350
+ ['link', 'image', 'table', 'embedMedia', 'blockquote', 'codeBlock', 'horizontalRule'],
351
+ ['subscript', 'superscript'],
352
+ ['findReplace', 'toggleMarkdown', 'sourceMode', 'export', 'fullscreen'],
353
+ ]
354
+ ```
355
+
356
+ Each inner array is a visual group separated by a divider.
357
+
358
+ ### Custom Toolbar via Props
359
+
360
+ ```jsx
361
+ <RemyxEditor
362
+ toolbar={[
363
+ ['bold', 'italic', 'underline'],
364
+ ['orderedList', 'unorderedList'],
365
+ ['link', 'image'],
366
+ ]}
367
+ />
368
+ ```
369
+
370
+ ### Toolbar Presets
371
+
372
+ ```jsx
373
+ import { TOOLBAR_PRESETS } from '@remyxjs/react';
374
+
375
+ <RemyxEditor toolbar={TOOLBAR_PRESETS.full} /> // all items including plugin commands (default)
376
+ <RemyxEditor toolbar={TOOLBAR_PRESETS.rich} /> // all features with callout, math, toc, bookmark, merge tag
377
+ <RemyxEditor toolbar={TOOLBAR_PRESETS.standard} /> // common editing without plugins
378
+ <RemyxEditor toolbar={TOOLBAR_PRESETS.minimal} /> // headings, basics, lists, links, images
379
+ <RemyxEditor toolbar={TOOLBAR_PRESETS.bare} /> // bold, italic, underline only
380
+ ```
381
+
382
+ ### Toolbar Helper Functions
383
+
384
+ #### `removeToolbarItems(config, itemsToRemove)`
385
+
386
+ ```jsx
387
+ import { DEFAULT_TOOLBAR, removeToolbarItems } from '@remyxjs/react';
388
+
389
+ const toolbar = removeToolbarItems(DEFAULT_TOOLBAR, ['image', 'table', 'embedMedia', 'export']);
390
+ ```
391
+
392
+ #### `addToolbarItems(config, items, options?)`
393
+
394
+ ```jsx
395
+ import { TOOLBAR_PRESETS, addToolbarItems } from '@remyxjs/react';
396
+
397
+ addToolbarItems(TOOLBAR_PRESETS.minimal, ['fullscreen']) // append as new group
398
+ addToolbarItems(TOOLBAR_PRESETS.minimal, 'fullscreen', { group: -1 }) // add to last group
399
+ addToolbarItems(TOOLBAR_PRESETS.minimal, 'taskList', { after: 'unorderedList' })
400
+ addToolbarItems(TOOLBAR_PRESETS.minimal, 'strikethrough', { before: 'underline' })
401
+ ```
402
+
403
+ #### `createToolbar(items)`
404
+
405
+ Build a toolbar from a flat list — items are auto-grouped by category:
406
+
407
+ ```jsx
408
+ import { createToolbar } from '@remyxjs/react';
409
+
410
+ const toolbar = createToolbar([
411
+ 'bold', 'italic', 'underline', 'headings',
412
+ 'link', 'image', 'orderedList', 'unorderedList', 'fullscreen',
413
+ ]);
414
+ // Groups by category: [['headings'], ['bold', 'italic', 'underline'], ['orderedList', 'unorderedList'], ['link', 'image'], ['fullscreen']]
415
+ ```
416
+
417
+ ### Available Toolbar Items
418
+
419
+ | Item | Type | Description |
420
+ | --- | --- | --- |
421
+ | `undo` | Button | Undo last action |
422
+ | `redo` | Button | Redo last action |
423
+ | `headings` | Dropdown | Block type (Normal, H1–H6) |
424
+ | `fontFamily` | Dropdown | Font family selector |
425
+ | `fontSize` | Dropdown | Font size selector |
426
+ | `bold` | Button | Bold |
427
+ | `italic` | Button | Italic |
428
+ | `underline` | Button | Underline |
429
+ | `strikethrough` | Button | Strikethrough |
430
+ | `subscript` | Button | Subscript |
431
+ | `superscript` | Button | Superscript |
432
+ | `foreColor` | Color Picker | Text color |
433
+ | `backColor` | Color Picker | Background color |
434
+ | `alignLeft` | Button | Align left |
435
+ | `alignCenter` | Button | Align center |
436
+ | `alignRight` | Button | Align right |
437
+ | `alignJustify` | Button | Justify |
438
+ | `orderedList` | Button | Numbered list |
439
+ | `unorderedList` | Button | Bulleted list |
440
+ | `taskList` | Button | Task/checkbox list |
441
+ | `indent` | Button | Increase indent |
442
+ | `outdent` | Button | Decrease indent |
443
+ | `link` | Button | Insert/edit link |
444
+ | `image` | Button | Insert image |
445
+ | `attachment` | Button | Attach file |
446
+ | `importDocument` | Button | Import (PDF, DOCX, MD, CSV) |
447
+ | `table` | Button | Insert table |
448
+ | `embedMedia` | Button | Embed video/media |
449
+ | `blockquote` | Button | Block quote |
450
+ | `codeBlock` | Button | Code block |
451
+ | `horizontalRule` | Button | Horizontal divider |
452
+ | `findReplace` | Button | Find and replace |
453
+ | `toggleMarkdown` | Button | Toggle markdown mode |
454
+ | `sourceMode` | Button | View/edit HTML source |
455
+ | `export` | Button | Export (PDF, MD, DOCX) |
456
+ | `commandPalette` | Button | Open command palette |
457
+ | `fullscreen` | Button | Toggle fullscreen |
458
+ | `insertCallout` | Button | Insert callout block |
459
+ | `insertMath` | Button | Insert math equation |
460
+ | `insertToc` | Button | Insert table of contents |
461
+ | `insertBookmark` | Button | Insert bookmark anchor |
462
+ | `insertMergeTag` | Button | Insert merge tag |
463
+ | `toggleAnalytics` | Button | Toggle analytics panel |
464
+ | `toggleSpellcheck` | Button | Toggle spellcheck |
465
+ | `checkGrammar` | Button | Run grammar check |
466
+ | `addComment` | Button | Add inline comment on selection |
467
+ | `removeFormat` | Button | Remove all inline formatting |
468
+ | `distractionFree` | Button | Toggle distraction-free mode (Mod+Shift+D) |
469
+ | `toggleSplitView` | Button | Toggle side-by-side preview (Mod+Shift+V) |
470
+ | `typography` | Dropdown | Typography controls (line height, letter spacing, paragraph spacing) |
471
+ | `lineHeight` | Dropdown | Line height adjustment |
472
+ | `letterSpacing` | Dropdown | Letter spacing adjustment |
473
+ | `paragraphSpacing` | Dropdown | Paragraph spacing adjustment |
474
+ | `startCollaboration` | Button | Start real-time collaboration session |
475
+ | `stopCollaboration` | Button | Stop collaboration session |
476
+
477
+ ## Menu Bar
478
+
479
+ Add an application-style menu bar above the toolbar. Both the toolbar and menu bar display all their items — the menu bar is an additional navigation layer, not a replacement for the toolbar.
480
+
481
+ ### Enable
482
+
483
+ ```jsx
484
+ <RemyxEditor menuBar={true} />
485
+ ```
486
+
487
+ **Default menus:**
488
+
489
+ | Menu | Items |
490
+ | --- | --- |
491
+ | **File** | Import Document, Export Document |
492
+ | **Edit** | Undo, Redo, Find & Replace |
493
+ | **View** | Fullscreen, Distraction-Free Mode, Split View, Toggle Markdown, Source Mode, Toggle Analytics |
494
+ | **Insert** | Link, Image, Table, Attachment, Embed Media, Blockquote, Code Block, Horizontal Rule, Insert Callout, Insert Math, Insert TOC, Insert Bookmark, Insert Merge Tag, Add Comment |
495
+ | **Format** | Bold, Italic, Underline, Strikethrough, Subscript, Superscript, Alignment, Lists, Colors, Typography (Line Height, Letter Spacing, Paragraph Spacing), Remove Formatting |
496
+
497
+ ### Custom Menu Bar
498
+
499
+ ```jsx
500
+ <RemyxEditor
501
+ menuBar={[
502
+ { label: 'File', items: ['importDocument', 'export'] },
503
+ { label: 'Edit', items: ['undo', 'redo', '---', 'findReplace'] },
504
+ { label: 'Format', items: [
505
+ 'bold', 'italic', 'underline',
506
+ '---',
507
+ { label: 'Alignment', items: ['alignLeft', 'alignCenter', 'alignRight', 'alignJustify'] },
508
+ ]},
509
+ ]}
510
+ />
511
+ ```
512
+
513
+ Each menu object has a `label` (string) and `items` array of command names, `'---'` separators, or nested `{ label, items }` submenu objects. Any command from the [Available Toolbar Items](#available-toolbar-items) table works as a menu item.
514
+
515
+ ### Menu Bar with Config File
516
+
517
+ ```js
518
+ export default defineConfig({
519
+ menuBar: true,
520
+ editors: {
521
+ full: { menuBar: true },
522
+ minimal: {
523
+ menuBar: [
524
+ { label: 'Edit', items: ['undo', 'redo'] },
525
+ { label: 'Format', items: ['bold', 'italic', 'underline'] },
526
+ ],
527
+ toolbar: [['headings', 'fontFamily', 'fontSize']],
528
+ },
529
+ },
530
+ });
531
+ ```
532
+
533
+ ### Toolbar and Menu Bar
534
+
535
+ When both the toolbar and menu bar are enabled, the full toolbar is preserved. The menu bar provides an additional navigation layer — it does not remove items from the toolbar. Both surfaces can be used together to give users maximum flexibility.
536
+
537
+ ### Menu Bar Behavior
538
+
539
+ - **Click** a trigger to open, **hover** to switch between open menus
540
+ - **Escape** or click outside to close
541
+ - Submenus open on hover with a chevron
542
+ - Toggle commands show active state, modal commands open their dialogs
543
+ - Inherits the editor's theme and custom theme variables
544
+
545
+ ## Multiple Editors
546
+
547
+ Multiple `<RemyxEditor />` instances work on the same page without any additional setup. Each editor is fully isolated.
548
+
549
+ ### Basic Usage
550
+
551
+ ```jsx
552
+ <RemyxEditor placeholder="Editor 1..." height={300} />
553
+ <RemyxEditor placeholder="Editor 2..." height={300} />
554
+ <RemyxEditor placeholder="Editor 3..." height={200} />
555
+ ```
556
+
557
+ ### What's Isolated Per Instance
558
+
559
+ | Feature | Isolation |
560
+ | --- | --- |
561
+ | **Content & state** | Separate undo/redo history, content, and selection |
562
+ | **Toolbar** | Independent dropdowns, color pickers, and active states |
563
+ | **Menu bar** | Menus open/close independently per editor |
564
+ | **Modals** | Each editor has its own modals (link, image, table, find/replace) |
565
+ | **Floating toolbar** | Appears only for the editor with an active selection |
566
+ | **Context menu** | Scoped to the clicked editor |
567
+ | **Fullscreen** | Each editor enters/exits fullscreen independently |
568
+ | **Plugins** | Per-editor plugin instances with separate state |
569
+ | **Events** | Separate `EventBus` per instance |
570
+ | **Keyboard shortcuts** | Fire only for the focused editor |
571
+
572
+ ### Mixed Configurations
573
+
574
+ ```jsx
575
+ <RemyxConfigProvider config={config}>
576
+ <RemyxEditor menuBar={true} height={400} />
577
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
578
+ <RemyxEditor config="minimal" />
579
+ <RemyxEditor config="comments" />
580
+ </div>
581
+ <RemyxEditor theme="light" placeholder="Standalone editor" />
582
+ </RemyxConfigProvider>
583
+ ```
584
+
585
+ No hard limit on editor count. Performance scales linearly. Editors in a `RemyxConfigProvider` share the config (read-only) but all runtime state is per-instance.
586
+
587
+ ## Font Configuration
588
+
589
+ ### Custom Font List
590
+
591
+ ```jsx
592
+ <RemyxEditor fonts={['Arial', 'Georgia', 'Courier New', 'My Custom Font']} />
593
+ ```
594
+
595
+ ### Google Fonts
596
+
597
+ ```jsx
598
+ <RemyxEditor googleFonts={['Roboto', 'Open Sans', 'Lato', 'Montserrat']} />
599
+
600
+ // With custom fonts
601
+ <RemyxEditor fonts={['Arial', 'Georgia']} googleFonts={['Poppins', 'Inter']} />
602
+
603
+ // Specific weights
604
+ <RemyxEditor googleFonts={['Roboto:wght@400;700']} />
605
+ ```
606
+
607
+ Google Fonts are auto-loaded via CDN and merged into the font dropdown.
608
+
609
+ ### Font Helper Functions
610
+
611
+ ```jsx
612
+ import { DEFAULT_FONTS, removeFonts, addFonts, loadGoogleFonts } from '@remyxjs/react';
613
+
614
+ // Remove fonts
615
+ const fonts = removeFonts(DEFAULT_FONTS, ['Comic Sans MS', 'Impact']);
616
+
617
+ // Add fonts (append by default, or { position: 'start' } to prepend)
618
+ const fonts = addFonts(DEFAULT_FONTS, ['My Custom Font']);
619
+
620
+ // Load Google Fonts programmatically
621
+ loadGoogleFonts(['Roboto', 'Lato']);
622
+ ```
623
+
624
+ ## Status Bar
625
+
626
+ The status bar displays word and character counts.
627
+
628
+ ```jsx
629
+ <RemyxEditor statusBar="bottom" /> {/* below content (default) */}
630
+ <RemyxEditor statusBar="top" /> {/* above content */}
631
+ <RemyxEditor statusBar="popup" /> {/* toolbar button with popover */}
632
+ <RemyxEditor statusBar={false} /> {/* hidden */}
633
+ ```
634
+
635
+ ## Theming
636
+
637
+ ```jsx
638
+ <RemyxEditor theme="light" /> // default
639
+ <RemyxEditor theme="dark" />
640
+ ```
641
+
642
+ ### Custom Themes
643
+
644
+ Override CSS variables using `createTheme()` or raw variable names. Custom themes layer on top of the base theme — only specify what you want to change.
645
+
646
+ ```jsx
647
+ import { createTheme } from '@remyxjs/react';
648
+
649
+ const brandTheme = createTheme({
650
+ bg: '#1a1a2e',
651
+ text: '#e0e0e0',
652
+ primary: '#e94560',
653
+ primaryHover: '#c81e45',
654
+ toolbarBg: '#16213e',
655
+ toolbarIcon: '#94a3b8',
656
+ toolbarIconActive: '#e94560',
657
+ radius: '12px',
658
+ contentFontSize: '18px',
659
+ });
660
+
661
+ <RemyxEditor theme="dark" customTheme={brandTheme} />
662
+ ```
663
+
664
+ Or pass raw CSS variables directly:
665
+
666
+ ```jsx
667
+ <RemyxEditor customTheme={{ '--rmx-primary': '#e94560', '--rmx-bg': '#1a1a2e' }} />
668
+ ```
669
+
670
+ ### Theme Presets
671
+
672
+ All 6 built-in themes are available via the `theme` prop:
673
+
674
+ ```jsx
675
+ <RemyxEditor theme="light" /> {/* Default — clean white */}
676
+ <RemyxEditor theme="dark" /> {/* Neutral dark */}
677
+ <RemyxEditor theme="ocean" /> {/* Deep blue */}
678
+ <RemyxEditor theme="forest" /> {/* Green earth-tone */}
679
+ <RemyxEditor theme="sunset" /> {/* Warm orange/amber */}
680
+ <RemyxEditor theme="rose" /> {/* Soft pink */}
681
+ ```
682
+
683
+ Override individual variables on top of any theme with `customTheme`:
684
+
685
+ ```jsx
686
+ import { createTheme } from '@remyxjs/react';
687
+
688
+ <RemyxEditor theme="ocean" customTheme={createTheme({ primary: '#ff6b6b' })} />
689
+ ```
690
+
691
+ ### Available Theme Variables
692
+
693
+ | Key | CSS Variable | Description |
694
+ | --- | --- | --- |
695
+ | `bg` | `--rmx-bg` | Editor background |
696
+ | `text` | `--rmx-text` | Primary text color |
697
+ | `textSecondary` | `--rmx-text-secondary` | Muted text color |
698
+ | `border` | `--rmx-border` | Border color |
699
+ | `borderSubtle` | `--rmx-border-subtle` | Subtle border color |
700
+ | `toolbarBg` | `--rmx-toolbar-bg` | Toolbar background |
701
+ | `toolbarBorder` | `--rmx-toolbar-border` | Toolbar border |
702
+ | `toolbarButtonHover` | `--rmx-toolbar-button-hover` | Button hover background |
703
+ | `toolbarButtonActive` | `--rmx-toolbar-button-active` | Button active background |
704
+ | `toolbarIcon` | `--rmx-toolbar-icon` | Icon color |
705
+ | `toolbarIconActive` | `--rmx-toolbar-icon-active` | Active icon color |
706
+ | `primary` | `--rmx-primary` | Primary accent color |
707
+ | `primaryHover` | `--rmx-primary-hover` | Primary hover color |
708
+ | `primaryLight` | `--rmx-primary-light` | Light primary (backgrounds) |
709
+ | `focusRing` | `--rmx-focus-ring` | Focus outline color |
710
+ | `selection` | `--rmx-selection` | Text selection color |
711
+ | `danger` | `--rmx-danger` | Error/danger color |
712
+ | `dangerLight` | `--rmx-danger-light` | Light danger color |
713
+ | `placeholder` | `--rmx-placeholder` | Placeholder text color |
714
+ | `modalBg` | `--rmx-modal-bg` | Modal background |
715
+ | `modalOverlay` | `--rmx-modal-overlay` | Modal overlay |
716
+ | `statusbarBg` | `--rmx-statusbar-bg` | Status bar background |
717
+ | `statusbarText` | `--rmx-statusbar-text` | Status bar text |
718
+ | `fontFamily` | `--rmx-font-family` | UI font stack |
719
+ | `fontSize` | `--rmx-font-size` | UI font size |
720
+ | `contentFontSize` | `--rmx-content-font-size` | Content font size |
721
+ | `contentLineHeight` | `--rmx-content-line-height` | Content line height |
722
+ | `radius` | `--rmx-radius` | Border radius |
723
+ | `radiusSm` | `--rmx-radius-sm` | Small border radius |
724
+ | `spacingXs` | `--rmx-spacing-xs` | Extra small spacing |
725
+ | `spacingSm` | `--rmx-spacing-sm` | Small spacing |
726
+ | `spacingMd` | `--rmx-spacing-md` | Medium spacing |
727
+
728
+ ### Per-Item Toolbar Theming
729
+
730
+ Style individual toolbar buttons independently using `toolbarItemTheme`.
731
+
732
+ ```jsx
733
+ import { createToolbarItemTheme } from '@remyxjs/react';
734
+
735
+ const itemTheme = createToolbarItemTheme({
736
+ bold: { color: '#e11d48', activeColor: '#be123c', activeBackground: '#ffe4e6', borderRadius: '50%' },
737
+ italic: { color: '#7c3aed', activeColor: '#6d28d9', activeBackground: '#ede9fe' },
738
+ underline: { color: '#0891b2', activeColor: '#0e7490', activeBackground: '#cffafe' },
739
+ _separator: { color: '#c4b5fd', width: '2px' },
740
+ });
741
+
742
+ <RemyxEditor toolbarItemTheme={itemTheme} />
743
+ ```
744
+
745
+ Both `customTheme` and `toolbarItemTheme` can be used together — `customTheme` sets global styles, `toolbarItemTheme` overrides specific items.
746
+
747
+ **Per-item style properties:**
748
+
749
+ | Key | CSS Variable | Description |
750
+ | --- | --- | --- |
751
+ | `color` | `--rmx-tb-color` | Icon/text color |
752
+ | `background` | `--rmx-tb-bg` | Default background |
753
+ | `hoverColor` | `--rmx-tb-hover-color` | Color on hover |
754
+ | `hoverBackground` | `--rmx-tb-hover-bg` | Background on hover |
755
+ | `activeColor` | `--rmx-tb-active-color` | Color when active |
756
+ | `activeBackground` | `--rmx-tb-active-bg` | Background when active |
757
+ | `border` | `--rmx-tb-border` | Border shorthand |
758
+ | `borderRadius` | `--rmx-tb-radius` | Border radius |
759
+ | `size` | `--rmx-tb-size` | Button width & height |
760
+ | `iconSize` | `--rmx-tb-icon-size` | Icon size |
761
+ | `padding` | `--rmx-tb-padding` | Button padding |
762
+ | `opacity` | `--rmx-tb-opacity` | Button opacity |
763
+
764
+ **Separator properties** (via `_separator` key):
765
+
766
+ | Key | CSS Variable | Description |
767
+ | --- | --- | --- |
768
+ | `color` | `--rmx-tb-sep-color` | Separator color |
769
+ | `width` | `--rmx-tb-sep-width` | Separator width |
770
+ | `height` | `--rmx-tb-sep-height` | Separator height |
771
+ | `margin` | `--rmx-tb-sep-margin` | Separator margin |
772
+
773
+ ## Paste & Auto-Conversion
774
+
775
+ The editor cleans and normalizes pasted content automatically.
776
+
777
+ | Source | Handling |
778
+ | --- | --- |
779
+ | **Microsoft Word** | Strips Office XML, `mso-*` styles, conditional comments. Converts Word-style lists to proper `<ul>`/`<ol>`. |
780
+ | **Google Docs** | Removes internal IDs/classes. Converts styled spans to semantic tags (`<strong>`, `<em>`, `<s>`). |
781
+ | **LibreOffice** | Strips namespace tags and auto-generated class names. |
782
+ | **Apple Pages** | Removes iWork-specific attributes. |
783
+ | **Markdown** | Auto-detects and converts to rich HTML (headings, lists, bold, links, code fences, etc.). |
784
+ | **Plain text** | Wraps in `<p>` tags with `<br>` for line breaks. |
785
+ | **Images** | Inserted as base64 data URIs, or uploaded via `uploadHandler` if configured. |
786
+
787
+ All paste paths (keyboard, drag-and-drop, context menu) share the same pipeline: HTML goes through `cleanPastedHTML()` then `Sanitizer.sanitize()`. Plain text is checked by `looksLikeMarkdown()` and converted if detected.
788
+
789
+ ```js
790
+ import { cleanPastedHTML, looksLikeMarkdown } from '@remyxjs/react';
791
+
792
+ const clean = cleanPastedHTML(dirtyHtml);
793
+ const isMarkdown = looksLikeMarkdown(text);
794
+ ```
795
+
796
+ ## File Uploads
797
+
798
+ The `uploadHandler` prop controls where images and file attachments are stored. It receives a `File` and returns a `Promise<string>` with the file URL.
799
+
800
+ Without an `uploadHandler`, images are inserted as base64 data URIs and the attachment upload tab is disabled.
801
+
802
+ ### Custom Server
803
+
804
+ ```jsx
805
+ <RemyxEditor
806
+ uploadHandler={async (file) => {
807
+ const formData = new FormData();
808
+ formData.append('file', file);
809
+ const res = await fetch('/api/upload', { method: 'POST', body: formData });
810
+ const { url } = await res.json();
811
+ return url;
812
+ }}
813
+ />
814
+ ```
815
+
816
+ ### S3 / R2 / GCS (Pre-signed URL)
817
+
818
+ ```jsx
819
+ <RemyxEditor
820
+ uploadHandler={async (file) => {
821
+ const res = await fetch('/api/presign', {
822
+ method: 'POST',
823
+ headers: { 'Content-Type': 'application/json' },
824
+ body: JSON.stringify({ filename: file.name, contentType: file.type }),
825
+ });
826
+ const { uploadUrl, publicUrl } = await res.json();
827
+ await fetch(uploadUrl, { method: 'PUT', headers: { 'Content-Type': file.type }, body: file });
828
+ return publicUrl;
829
+ }}
830
+ />
831
+ ```
832
+
833
+ ### Upload Paths
834
+
835
+ The handler is called consistently across all upload surfaces:
836
+
837
+ | Path | Result |
838
+ | --- | --- |
839
+ | Image toolbar (Upload tab) | Inserted as `<img>` |
840
+ | Attachment toolbar (Upload tab) | Inserted as attachment chip |
841
+ | Drag & drop image/file | Image as `<img>`, others as attachment |
842
+ | Paste image/file | Same as drag & drop |
843
+
844
+ ## Output Formats
845
+
846
+ ```jsx
847
+ <RemyxEditor outputFormat="html" onChange={(html) => console.log(html)} />
848
+ <RemyxEditor outputFormat="markdown" onChange={(md) => console.log(md)} />
849
+ ```
850
+
851
+ Conversion utilities for standalone use:
852
+
853
+ ```js
854
+ import { htmlToMarkdown, markdownToHtml } from '@remyxjs/react';
855
+
856
+ const md = htmlToMarkdown('<h1>Hello</h1><p>World</p>');
857
+ const html = markdownToHtml('# Hello\n\nWorld');
858
+ ```
859
+
860
+ ## Attaching to Existing Elements
861
+
862
+ Enhance an existing `<textarea>` or `<div>` with a full WYSIWYG editor.
863
+
864
+ ### Textarea
865
+
866
+ ```jsx
867
+ const textareaRef = useRef(null);
868
+
869
+ <textarea ref={textareaRef} defaultValue="Initial content" />
870
+ <RemyxEditor attachTo={textareaRef} outputFormat="markdown" height={400} />
871
+ ```
872
+
873
+ The textarea is hidden and its value stays in sync for form submission.
874
+
875
+ ### Div
876
+
877
+ ```jsx
878
+ const divRef = useRef(null);
879
+
880
+ <div ref={divRef}>
881
+ <h2>Existing content</h2>
882
+ <p>This will become editable.</p>
883
+ </div>
884
+ <RemyxEditor attachTo={divRef} theme="dark" height={400} />
885
+ ```
886
+
887
+ ### `useRemyxEditor` Hook
888
+
889
+ For lower-level control:
890
+
891
+ ```jsx
892
+ import { useRemyxEditor } from '@remyxjs/react';
893
+
894
+ function MyEditor() {
895
+ const targetRef = useRef(null);
896
+ const { engine, containerRef, editableRef, ready } = useRemyxEditor(targetRef, {
897
+ onChange: (content) => console.log(content),
898
+ placeholder: 'Type here...',
899
+ theme: 'light',
900
+ height: 400,
901
+ });
902
+
903
+ return <textarea ref={targetRef} />;
904
+ }
905
+ ```
906
+
907
+ Returns `engine` (EditorEngine), `containerRef`, `editableRef`, and `ready` (boolean).
908
+
909
+ ### `useEditorEngine` Hook
910
+
911
+ The lowest-level hook — manages just the engine lifecycle:
912
+
913
+ ```jsx
914
+ import { useEditorEngine } from '@remyxjs/react';
915
+
916
+ function CustomEditor() {
917
+ const editorRef = useRef(null);
918
+ const engine = useEditorEngine(editorRef, {
919
+ outputFormat: 'html',
920
+ plugins: [],
921
+ });
922
+
923
+ return <div ref={editorRef} contentEditable />;
924
+ }
925
+ ```
926
+
927
+ ## Plugins
928
+
929
+ ### Built-in Plugins
930
+
931
+ Fifteen built-in plugins are available:
932
+
933
+ - **WordCountPlugin** — word/character counts in the status bar
934
+ - **PlaceholderPlugin** — placeholder text when empty
935
+ - **AutolinkPlugin** — auto-converts typed URLs to links
936
+ - **SyntaxHighlightPlugin** — automatic syntax highlighting for code blocks with language detection, theme-aware colors, `setCodeLanguage`/`getCodeLanguage`/`toggleLineNumbers` commands, copy-to-clipboard button, inline `<code>` mini-highlighting, and extensible language registry (`registerLanguage`/`unregisterLanguage`). When active, a language selector dropdown appears on focused code blocks.
937
+ - **TablePlugin** — enhanced table features including column/row resize handles, click-to-sort on header cells (single + multi-column with Shift), per-column filter dropdowns, inline cell formulas (SUM, AVERAGE, COUNT, MIN, MAX, IF, CONCAT), cell formatting (number, currency, percentage, date), and sticky header rows. Adds 6 new commands: `toggleHeaderRow`, `sortTable`, `filterTable`, `clearTableFilters`, `formatCell`, `evaluateFormulas`. Context menu adds Toggle Header Row, Format Cell options, and Clear Filters when right-clicking in a table.
938
+
939
+ - **CalloutPlugin** — styled callout blocks with 7 built-in types (info, warning, error, success, tip, note, question), custom type registration, collapsible toggle, nested content, GFM alert syntax auto-conversion. Adds 4 commands: `insertCallout`, `removeCallout`, `changeCalloutType`, `toggleCalloutCollapse`. Context menu adds "Insert Callout" and "Remove Callout".
940
+ - **CommentsPlugin** — inline comment threads on selected text, resolved/unresolved state with visual indicators, @mention parsing, reply threads, comment-only mode (read-only editor with annotation support), import/export, DOM sync via MutationObserver. Adds 6 new commands: `addComment`, `deleteComment`, `resolveComment`, `replyToComment`, `editComment`, `navigateToComment`. Context menu adds "Add Comment" when text is selected.
941
+ - **LinkPlugin** — link preview tooltips (via `onUnfurl` callback), broken link detection with periodic scanning and `rmx-link-broken` indicators, auto-linking of typed URLs/emails/phone numbers on Space/Enter, link click analytics (`onLinkClick`), bookmark anchors (`insertBookmark`, `linkToBookmark`, `getBookmarks`, `removeBookmark`), `scanBrokenLinks`/`getBrokenLinks` commands.
942
+ - **TemplatePlugin** — `{{merge_tag}}` syntax with visual chips, `{{#if condition}}...{{/if}}` conditional blocks, `{{#each items}}...{{/each}}` repeatable sections, live preview with sample data, 5 pre-built templates (email, invoice, letter, report, newsletter), `registerTemplate`/`unregisterTemplate`/`getTemplateLibrary`/`getTemplate` APIs, `exportTemplate` JSON export. Adds 6 commands: `insertMergeTag`, `loadTemplate`, `previewTemplate`, `exitPreview`, `exportTemplate`, `getTemplateTags`.
943
+ - **KeyboardPlugin** — Vim mode (normal/insert/visual with h/j/k/l/w/b motions), Emacs keybindings (Ctrl+A/E/F/B/K/Y), custom keybinding map, smart bracket/quote auto-pairing with wrap-selection, multi-cursor (`Cmd+D`), jump-to-heading (`Cmd+Shift+G`). Adds 5 commands: `setKeyboardMode`, `getVimMode`, `jumpToHeading`, `getHeadings`, `selectNextOccurrence`.
944
+ - **DragDropPlugin** — drop zone overlays with visual guides, cross-editor drag via `text/x-remyx-block` transfer, external file/image/rich-text drops, block reorder with ghost preview and drop indicator, `onDrop`/`onFileDrop` callbacks. Adds 2 commands: `moveBlockUp`, `moveBlockDown` (`Cmd+Shift+Arrow`).
945
+ - **MathPlugin** — LaTeX/KaTeX math rendering with `$...$` inline and `$$...$$` block syntax, pluggable `renderMath(latex, displayMode)` callback, 40+ symbol palette across 4 categories (Greek, Operators, Arrows, Common), auto equation numbering, `latexToMathML()` conversion, `parseMathExpressions()` detection. Adds 6 commands: `insertMath`, `editMath`, `insertSymbol`, `getSymbolPalette`, `getMathElements`, `copyMathAs`.
946
+ - **TocPlugin** — auto-generated table of contents from H1-H6 heading hierarchy, section numbering (1, 1.1, 1.2), `insertToc` renders linked `<nav>` into document, `scrollToHeading` with smooth scroll, `validateHeadings` detects skipped levels, `onOutlineChange` callback + `toc:change` event. Adds 4 commands: `getOutline`, `insertToc`, `scrollToHeading`, `validateHeadings`.
947
+ - **AnalyticsPlugin** — real-time readability scores (Flesch-Kincaid, Gunning Fog, Coleman-Liau, Flesch Reading Ease), reading time estimate, vocabulary level (basic/intermediate/advanced), sentence/paragraph length warnings, goal-based word count with progress tracking, keyword density, SEO hints. `onAnalytics` callback + `analytics:update` event. Adds 3 commands: `getAnalytics`, `getSeoAnalysis`, `getKeywordDensity`.
948
+ - **SpellcheckPlugin** — built-in grammar engine with passive voice detection, wordiness patterns, cliche detection, and punctuation checks. Inline red wavy (spelling), blue wavy (grammar), and green dotted (style) underlines. Right-click context menu with correction suggestions, "Ignore", and "Add to Dictionary". Writing-style presets (formal, casual, technical, academic). BCP 47 multi-language support. Optional `customService` interface for LanguageTool/Grammarly. Persistent dictionary via localStorage. `onError`/`onCorrection` callbacks + `spellcheck:update`/`grammar:check` events. Adds 6 commands: `toggleSpellcheck`, `checkGrammar`, `addToDictionary`, `ignoreWord`, `setWritingStyle`, `getSpellcheckStats`.
949
+
950
+ ```jsx
951
+ import { SyntaxHighlightPlugin, TablePlugin, CommentsPlugin, CalloutPlugin, LinkPlugin, TemplatePlugin, KeyboardPlugin, DragDropPlugin, MathPlugin, TocPlugin, AnalyticsPlugin, SpellcheckPlugin } from '@remyxjs/react';
952
+
953
+ <RemyxEditor
954
+ plugins={[SyntaxHighlightPlugin(), TablePlugin(), CommentsPlugin(), CalloutPlugin(), LinkPlugin(), TemplatePlugin(), KeyboardPlugin(), DragDropPlugin(), MathPlugin(), TocPlugin(), AnalyticsPlugin(), SpellcheckPlugin()]}
955
+ />
956
+ ```
957
+
958
+ #### TablePlugin in depth
959
+
960
+ When `TablePlugin()` is active, every `<table class="rmx-table">` in the editor automatically gains:
961
+
962
+ | Feature | How it works |
963
+ | --- | --- |
964
+ | **Sortable columns** | Click any `<th>` to cycle through ascending → descending → unsorted. Rows are physically reordered in the DOM so the sort persists in HTML output. |
965
+ | **Multi-column sort** | Hold **Shift** and click additional headers. A small priority number appears on each sorted header. |
966
+ | **Sort data types** | The sort auto-detects numeric, date, or alphabetical data. Pass `dataType` explicitly or provide a custom comparator via the `tableSortComparator` engine option. |
967
+ | **Sort indicators** | ▲/▼ triangles rendered via CSS `::after` on `<th>` elements with `data-sort-dir` attributes. |
968
+ | **Filterable rows** | A small ▽ icon appears in each header cell. Click it to open a filter dropdown with a text input. Rows not matching the filter are hidden (non-destructive — they reappear when the filter is cleared). Multiple columns can be filtered simultaneously (AND logic). |
969
+ | **Column/row resize** | Invisible 6px drag handles appear at column and row borders. Drag to resize. The resize is smooth (rAF-driven) and creates an undo snapshot on mouseup. |
970
+ | **Inline formulas** | Type `=SUM(A1:A5)` in any cell. On blur, the formula is stored in a `data-formula` attribute and the computed result is displayed. On focus, the formula text is shown for editing. Supports SUM, AVERAGE, COUNT, MIN, MAX, IF, CONCAT, cell references (A1 notation), ranges, arithmetic, and comparison operators. Circular references are detected and display `#CIRC!`. |
971
+ | **Cell formatting** | Right-click a cell to format as Number, Currency, Percentage, or Date. The raw value is preserved in `data-raw-value`; the display uses `Intl.NumberFormat` / `Intl.DateTimeFormat`. |
972
+ | **Sticky header** | `<thead><th>` cells use `position: sticky` so the header row stays visible when scrolling tall tables. |
973
+ | **Clipboard interop** | Copying table content produces both HTML and TSV (tab-separated values). Pasting TSV or HTML tables from Excel / Google Sheets inserts data into the grid starting at the caret cell, auto-expanding rows and columns as needed. |
974
+
975
+ **Custom sort comparator:**
976
+
977
+ ```jsx
978
+ <RemyxEditor
979
+ plugins={[TablePlugin()]}
980
+ engineOptions={{
981
+ tableSortComparator: (a, b, dataType, columnIndex) => {
982
+ // Custom comparison logic — return negative, zero, or positive
983
+ return a.localeCompare(b);
984
+ },
985
+ }}
986
+ />
987
+ ```
988
+
989
+ **Programmatic commands (via engine ref):**
990
+
991
+ ```jsx
992
+ import { useRef } from 'react';
993
+
994
+ function MyEditor() {
995
+ const engineRef = useRef(null);
996
+
997
+ const handleSort = () => {
998
+ engineRef.current?.executeCommand('sortTable', {
999
+ keys: [
1000
+ { columnIndex: 0, direction: 'asc' },
1001
+ { columnIndex: 2, direction: 'desc', dataType: 'numeric' },
1002
+ ],
1003
+ });
1004
+ };
1005
+
1006
+ const handleFilter = () => {
1007
+ engineRef.current?.executeCommand('filterTable', {
1008
+ columnIndex: 1,
1009
+ filterValue: 'active',
1010
+ });
1011
+ };
1012
+
1013
+ return (
1014
+ <>
1015
+ <button onClick={handleSort}>Sort by Name then Score</button>
1016
+ <button onClick={handleFilter}>Show active only</button>
1017
+ <RemyxEditor
1018
+ plugins={[TablePlugin()]}
1019
+ onReady={(engine) => { engineRef.current = engine; }}
1020
+ />
1021
+ </>
1022
+ );
1023
+ }
1024
+ ```
1025
+
1026
+ **Context menu additions** (appear when right-clicking inside a table):
1027
+ - Toggle Header Row
1028
+ - Format as Number / Currency / Percentage / Date
1029
+ - Clear Filters
1030
+
1031
+ ### CalloutPlugin
1032
+
1033
+ The `CalloutPlugin` adds styled callout/alert/admonition blocks with collapsible toggle and GFM alert auto-conversion:
1034
+
1035
+ ```jsx
1036
+ import { CalloutPlugin } from '@remyxjs/react';
1037
+
1038
+ <RemyxEditor plugins={[CalloutPlugin()]} />
1039
+ ```
1040
+
1041
+ **Insert callouts programmatically:**
1042
+
1043
+ ```jsx
1044
+ // Basic callout
1045
+ engine.executeCommand('insertCallout', { type: 'warning' });
1046
+
1047
+ // Collapsible with custom title
1048
+ engine.executeCommand('insertCallout', { type: 'tip', collapsible: true, title: 'Pro tip' });
1049
+
1050
+ // With pre-populated content
1051
+ engine.executeCommand('insertCallout', {
1052
+ type: 'info',
1053
+ content: '<ul><li>Step 1</li><li>Step 2</li></ul>',
1054
+ });
1055
+
1056
+ // Change type of the focused callout
1057
+ engine.executeCommand('changeCalloutType', 'error');
1058
+
1059
+ // Register a custom callout type
1060
+ import { registerCalloutType } from '@remyxjs/react';
1061
+ registerCalloutType({ type: 'security', label: 'Security', icon: '🔒', color: '#dc2626' });
1062
+ ```
1063
+
1064
+ **GitHub-flavored alerts** — blockquotes with `> [!NOTE]`, `> [!WARNING]`, etc. are automatically converted to callout blocks on paste or typing.
1065
+
1066
+ **Available types:** `info`, `warning`, `error`, `success`, `tip`, `note`, `question` (plus any custom types).
1067
+
1068
+ ### CommentsPlugin
1069
+
1070
+ The `CommentsPlugin` adds inline comment threads — highlight text, attach discussion threads, resolve/reopen, reply, and @mention other users.
1071
+
1072
+ ```jsx
1073
+ import { CommentsPlugin } from '@remyxjs/react';
1074
+
1075
+ <RemyxEditor
1076
+ plugins={[CommentsPlugin({
1077
+ onComment: (thread) => saveToServer(thread),
1078
+ onResolve: ({ thread, resolved }) => updateServer(thread),
1079
+ onDelete: (thread) => deleteFromServer(thread),
1080
+ onReply: ({ thread, reply }) => notifyUser(reply),
1081
+ mentionUsers: ['alice', 'bob', 'charlie'],
1082
+ })]}
1083
+ />
1084
+ ```
1085
+
1086
+ **Comment-only mode** — make the editor read-only but still allow annotations:
1087
+
1088
+ ```jsx
1089
+ <RemyxEditor
1090
+ plugins={[CommentsPlugin({ commentOnly: true })]}
1091
+ />
1092
+ ```
1093
+
1094
+ #### useComments hook
1095
+
1096
+ The `useComments` hook provides reactive state and convenience methods:
1097
+
1098
+ ```jsx
1099
+ import { useComments, CommentsPanel } from '@remyxjs/react';
1100
+
1101
+ function AnnotatedEditor() {
1102
+ const engineRef = useRef(null);
1103
+ const {
1104
+ threads, activeThread, addComment, deleteComment,
1105
+ resolveComment, replyToComment, navigateToComment,
1106
+ exportThreads, importThreads,
1107
+ } = useComments(engineRef.current);
1108
+
1109
+ return (
1110
+ <div style={{ display: 'flex' }}>
1111
+ <RemyxEditor
1112
+ plugins={[CommentsPlugin({ mentionUsers: ['alice', 'bob'] })]}
1113
+ onReady={(engine) => { engineRef.current = engine; }}
1114
+ style={{ flex: 1 }}
1115
+ />
1116
+ <CommentsPanel
1117
+ threads={threads}
1118
+ activeThread={activeThread}
1119
+ onNavigate={navigateToComment}
1120
+ onResolve={(id, resolved) => resolveComment(id, resolved)}
1121
+ onDelete={deleteComment}
1122
+ onReply={(id, params) => replyToComment(id, params)}
1123
+ filter="all" {/* 'all' | 'open' | 'resolved' */}
1124
+ />
1125
+ </div>
1126
+ );
1127
+ }
1128
+ ```
1129
+
1130
+ #### CommentsPanel props
1131
+
1132
+ | Prop | Type | Description |
1133
+ | --- | --- | --- |
1134
+ | `threads` | `CommentThread[]` | All threads (from `useComments`) |
1135
+ | `activeThread` | `CommentThread \| null` | Currently focused thread |
1136
+ | `onNavigate` | `(threadId) => void` | Called when a thread card is clicked |
1137
+ | `onResolve` | `(threadId, resolved) => void` | Called when Resolve/Reopen is clicked |
1138
+ | `onDelete` | `(threadId) => void` | Called when Delete is clicked |
1139
+ | `onReply` | `(threadId, { author, body }) => void` | Called when a reply is submitted |
1140
+ | `filter` | `'all' \| 'open' \| 'resolved'` | Which threads to display |
1141
+ | `className` | `string` | Additional CSS class |
1142
+
1143
+ #### Engine events
1144
+
1145
+ | Event | Payload | When |
1146
+ | --- | --- | --- |
1147
+ | `comment:created` | `{ thread }` | New comment added |
1148
+ | `comment:resolved` | `{ thread, resolved }` | Thread resolved or reopened |
1149
+ | `comment:deleted` | `{ thread }` | Thread deleted |
1150
+ | `comment:replied` | `{ thread, reply }` | Reply added to thread |
1151
+ | `comment:updated` | `{ thread }` | Thread body edited |
1152
+ | `comment:clicked` | `{ thread, element }` | Comment highlight clicked |
1153
+ | `comment:navigated` | `{ threadId }` | Scrolled to comment |
1154
+ | `comment:imported` | `{ count }` | Threads imported |
1155
+
1156
+ ### Custom Plugins
1157
+
1158
+ ```js
1159
+ import { createPlugin } from '@remyxjs/react';
1160
+
1161
+ const MyPlugin = createPlugin({
1162
+ name: 'my-plugin',
1163
+ version: '1.0.0',
1164
+ description: 'Example plugin with lifecycle hooks',
1165
+ author: 'You',
1166
+
1167
+ // Lifecycle hooks (auto-wired, sandboxed)
1168
+ onContentChange(api) {
1169
+ console.log('Content changed:', api.getText().length, 'chars');
1170
+ },
1171
+ onSelectionChange(api) {
1172
+ console.log('Selection:', api.getActiveFormats());
1173
+ },
1174
+
1175
+ // Traditional init/destroy
1176
+ init(api) { /* called once on mount */ },
1177
+ destroy(api) { /* called on unmount */ },
1178
+
1179
+ // Dependencies — ensures 'other-plugin' is initialized first
1180
+ dependencies: ['other-plugin'],
1181
+
1182
+ // Scoped settings with validation
1183
+ settingsSchema: [
1184
+ { key: 'maxLength', type: 'number', label: 'Max Length', defaultValue: 5000, validate: (v) => v > 0 },
1185
+ { key: 'mode', type: 'select', label: 'Mode', defaultValue: 'auto', options: [
1186
+ { label: 'Auto', value: 'auto' }, { label: 'Manual', value: 'manual' },
1187
+ ]},
1188
+ ],
1189
+ defaultSettings: { maxLength: 5000, mode: 'auto' },
1190
+ });
1191
+ ```
1192
+
1193
+ ```jsx
1194
+ <RemyxEditor plugins={[MyPlugin]} />
1195
+ ```
1196
+
1197
+ Access plugin settings from outside:
1198
+
1199
+ ```jsx
1200
+ <RemyxEditor
1201
+ plugins={[MyPlugin]}
1202
+ onReady={(engine) => {
1203
+ engine.plugins.getPluginSetting('my-plugin', 'maxLength'); // 5000
1204
+ engine.plugins.setPluginSetting('my-plugin', 'maxLength', 3000); // validates + updates
1205
+ }}
1206
+ />
1207
+ ```
1208
+
1209
+ ### Plugin with Custom Commands
1210
+
1211
+ ```js
1212
+ const HighlightPlugin = createPlugin({
1213
+ name: 'highlight',
1214
+ init(engine) {
1215
+ engine.executeCommand('highlight');
1216
+ },
1217
+ commands: [
1218
+ {
1219
+ name: 'highlight',
1220
+ execute(eng) {
1221
+ document.execCommand('backColor', false, '#ffeb3b');
1222
+ },
1223
+ },
1224
+ ],
1225
+ });
1226
+ ```
1227
+
1228
+ ## Read-Only Mode
1229
+
1230
+ Disable editing while keeping the full rendered output visible.
1231
+
1232
+ ```jsx
1233
+ <RemyxEditor value={content} readOnly={true} />
1234
+ ```
1235
+
1236
+ Combine with a toggle for preview mode:
1237
+
1238
+ ```jsx
1239
+ const [editing, setEditing] = useState(true);
1240
+
1241
+ <button onClick={() => setEditing(!editing)}>
1242
+ {editing ? 'Preview' : 'Edit'}
1243
+ </button>
1244
+ <RemyxEditor value={content} onChange={setContent} readOnly={!editing} />
1245
+ ```
1246
+
1247
+ ## Error Handling
1248
+
1249
+ ### onError Callback
1250
+
1251
+ Catch errors from plugins, the engine, and file uploads without crashing the app:
1252
+
1253
+ ```jsx
1254
+ <RemyxEditor
1255
+ onError={(error, info) => {
1256
+ if (info.source === 'plugin') {
1257
+ console.warn(`Plugin "${info.pluginName}" failed:`, error);
1258
+ } else if (info.source === 'upload') {
1259
+ alert(`Upload failed: ${error.message}`);
1260
+ } else if (info.source === 'engine') {
1261
+ console.error(`Engine error in ${info.phase}:`, error);
1262
+ }
1263
+ }}
1264
+ />
1265
+ ```
1266
+
1267
+ Error sources:
1268
+
1269
+ | Source | Info Fields | Description |
1270
+ |---|---|---|
1271
+ | `'plugin'` | `pluginName` | Plugin init/destroy failure |
1272
+ | `'engine'` | `phase` | Engine initialization error |
1273
+ | `'upload'` | `file` | Upload handler rejection |
1274
+
1275
+ ### Custom Error Fallback
1276
+
1277
+ Replace the default error UI with your own component:
1278
+
1279
+ ```jsx
1280
+ <RemyxEditor
1281
+ errorFallback={
1282
+ <div className="editor-error">
1283
+ <p>Something went wrong. Please refresh.</p>
1284
+ </div>
1285
+ }
1286
+ />
1287
+ ```
1288
+
1289
+ ## Engine Access
1290
+
1291
+ Use the `onReady` callback to access the `EditorEngine` instance for programmatic control.
1292
+
1293
+ ### Execute Commands
1294
+
1295
+ ```jsx
1296
+ function App() {
1297
+ const engineRef = useRef(null);
1298
+
1299
+ return (
1300
+ <>
1301
+ <button onClick={() => engineRef.current?.executeCommand('bold')}>
1302
+ Bold
1303
+ </button>
1304
+ <button onClick={() => engineRef.current?.executeCommand('insertLink', 'https://example.com')}>
1305
+ Insert Link
1306
+ </button>
1307
+ <RemyxEditor onReady={(engine) => { engineRef.current = engine }} />
1308
+ </>
1309
+ );
1310
+ }
1311
+ ```
1312
+
1313
+ ### Listen to Events
1314
+
1315
+ ```jsx
1316
+ <RemyxEditor
1317
+ onReady={(engine) => {
1318
+ engine.on('selection:change', ({ formats }) => {
1319
+ console.log('Active formats:', formats); // { bold: true, italic: false, ... }
1320
+ });
1321
+
1322
+ engine.on('command:executed', ({ name }) => {
1323
+ console.log('Command run:', name);
1324
+ });
1325
+ }}
1326
+ onFocus={() => console.log('Editor focused')}
1327
+ onBlur={() => console.log('Editor blurred')}
1328
+ />
1329
+ ```
1330
+
1331
+ ### Read Content Programmatically
1332
+
1333
+ ```jsx
1334
+ <RemyxEditor
1335
+ onReady={(engine) => {
1336
+ // HTML content
1337
+ const html = engine.getHTML();
1338
+
1339
+ // Plain text
1340
+ const text = engine.getText();
1341
+
1342
+ // Stats
1343
+ const words = engine.getWordCount();
1344
+ const chars = engine.getCharCount();
1345
+ const empty = engine.isEmpty();
1346
+ }}
1347
+ />
1348
+ ```
1349
+
1350
+ ### Engine Methods
1351
+
1352
+ | Method | Returns | Description |
1353
+ |---|---|---|
1354
+ | `getHTML()` | `string` | Sanitized HTML content |
1355
+ | `setHTML(html)` | `void` | Set editor content |
1356
+ | `getText()` | `string` | Plain text content |
1357
+ | `isEmpty()` | `boolean` | True if no meaningful text |
1358
+ | `focus()` | `void` | Focus the editor |
1359
+ | `blur()` | `void` | Blur the editor |
1360
+ | `executeCommand(name, ...args)` | `any` | Execute a registered command |
1361
+ | `on(event, handler)` | `Function` | Subscribe (returns unsubscribe fn) |
1362
+ | `off(event, handler)` | `void` | Unsubscribe from event |
1363
+ | `getWordCount()` | `number` | Word count |
1364
+ | `getCharCount()` | `number` | Character count |
1365
+ | `destroy()` | `void` | Clean up the engine |
1366
+
1367
+ ### Engine Events
1368
+
1369
+ | Event | Payload | When |
1370
+ |---|---|---|
1371
+ | `'content:change'` | — | Content was modified |
1372
+ | `'selection:change'` | `{ formats }` | Selection or formatting changed |
1373
+ | `'focus'` | — | Editor gained focus |
1374
+ | `'blur'` | — | Editor lost focus |
1375
+ | `'mode:change'` | — | Switched markdown/source mode |
1376
+ | `'command:executed'` | `{ name }` | After a command runs |
1377
+ | `'plugin:error'` | `{ name, error }` | Plugin failed |
1378
+ | `'editor:error'` | `{ phase, error }` | Engine error |
1379
+ | `'upload:error'` | `{ file, error }` | Upload failed |
1380
+
1381
+ ## Import & Export Documents
1382
+
1383
+ ### Programmatic Export
1384
+
1385
+ ```jsx
1386
+ import { exportAsPDF, exportAsDocx, exportAsMarkdown } from '@remyxjs/react';
1387
+
1388
+ function ExportButtons({ content }) {
1389
+ return (
1390
+ <>
1391
+ <button onClick={() => exportAsPDF(content, 'My Document')}>
1392
+ Export PDF
1393
+ </button>
1394
+ <button onClick={() => exportAsDocx(content, 'document.doc')}>
1395
+ Export Word
1396
+ </button>
1397
+ <button onClick={() => exportAsMarkdown(content, 'document.md')}>
1398
+ Export Markdown
1399
+ </button>
1400
+ </>
1401
+ );
1402
+ }
1403
+ ```
1404
+
1405
+ PDF export opens the browser print dialog. Word and Markdown exports trigger file downloads.
1406
+
1407
+ ### Programmatic Import
1408
+
1409
+ ```jsx
1410
+ import { convertDocument, isImportableFile } from '@remyxjs/react';
1411
+
1412
+ async function handleFileSelect(file, engine) {
1413
+ if (!isImportableFile(file)) {
1414
+ alert('Unsupported format');
1415
+ return;
1416
+ }
1417
+ const html = await convertDocument(file);
1418
+
1419
+ // Insert at cursor
1420
+ engine.executeCommand('importDocument', { html, mode: 'insert' });
1421
+
1422
+ // Or replace entire content
1423
+ engine.executeCommand('importDocument', { html, mode: 'replace' });
1424
+ }
1425
+ ```
1426
+
1427
+ Supported import formats: PDF, DOCX, Markdown, HTML, TXT, CSV, TSV, RTF.
1428
+
1429
+ ### Toolbar Export & Import
1430
+
1431
+ The editor ships with built-in toolbar buttons for both:
1432
+
1433
+ ```jsx
1434
+ <RemyxEditor
1435
+ toolbar={[
1436
+ ['bold', 'italic', 'underline'],
1437
+ ['importDocument', 'export'], // import & export buttons
1438
+ ]}
1439
+ />
1440
+ ```
1441
+
1442
+ The `importDocument` button opens a modal for file selection. The `export` button opens a modal with format options.
1443
+
1444
+ ## Sanitization
1445
+
1446
+ Control which HTML tags, attributes, and CSS properties are allowed in editor content.
1447
+
1448
+ ### Custom Allowlists
1449
+
1450
+ ```jsx
1451
+ <RemyxEditor
1452
+ sanitize={{
1453
+ allowedTags: {
1454
+ // Tag name → array of allowed attributes
1455
+ p: ['class', 'style'],
1456
+ strong: [],
1457
+ em: [],
1458
+ a: ['href', 'target', 'rel', 'title'],
1459
+ img: ['src', 'alt', 'width', 'height'],
1460
+ h1: ['class'], h2: ['class'], h3: ['class'],
1461
+ ul: [], ol: [], li: [],
1462
+ blockquote: ['class'],
1463
+ code: ['class'],
1464
+ pre: ['class'],
1465
+ },
1466
+ allowedStyles: [
1467
+ 'color', 'background-color', 'font-size', 'text-align',
1468
+ 'font-weight', 'font-style',
1469
+ ],
1470
+ }}
1471
+ />
1472
+ ```
1473
+
1474
+ ### Default Security
1475
+
1476
+ Without custom sanitization, the editor applies these protections by default:
1477
+
1478
+ - Strips `<script>`, `<style>`, `<svg>`, `<math>`, `<form>`, `<object>`, `<embed>` tags entirely
1479
+ - Blocks all `on*` event handler attributes (e.g., `onclick`, `onerror`)
1480
+ - Validates URL protocols — blocks `javascript:`, `vbscript:`, `data:text/html`
1481
+ - Blocks CSS injection (`expression()`, `@import`, `behavior:`)
1482
+ - Restricts `<input>` to `type="checkbox"` only
1483
+ - Sandboxes `<iframe>` elements
1484
+
1485
+ ## Keyboard Shortcuts
1486
+
1487
+ `mod` = `Cmd` on Mac, `Ctrl` on Windows/Linux.
1488
+
1489
+ | Shortcut | Action |
1490
+ | --- | --- |
1491
+ | `mod+B` | Bold |
1492
+ | `mod+I` | Italic |
1493
+ | `mod+U` | Underline |
1494
+ | `mod+Shift+X` | Strikethrough |
1495
+ | `mod+Z` | Undo |
1496
+ | `mod+Shift+Z` | Redo |
1497
+ | `mod+K` | Insert link |
1498
+ | `mod+F` | Find & replace |
1499
+ | `mod+Shift+7` | Numbered list |
1500
+ | `mod+Shift+8` | Bulleted list |
1501
+ | `mod+Shift+9` | Blockquote |
1502
+ | `mod+Shift+C` | Code block |
1503
+ | `Tab` | Indent |
1504
+ | `Shift+Tab` | Outdent |
1505
+ | `mod+Shift+F` | Fullscreen |
1506
+ | `mod+Shift+U` | Source mode |
1507
+ | `mod+Shift+P` | Command palette |
1508
+ | `mod+,` | Subscript |
1509
+ | `mod+.` | Superscript |
1510
+
1511
+ ### Custom Shortcuts
1512
+
1513
+ Override default shortcuts by passing a `shortcuts` object:
1514
+
1515
+ ```jsx
1516
+ <RemyxEditor
1517
+ shortcuts={{
1518
+ bold: 'mod+shift+b', // remap bold
1519
+ insertLink: 'mod+l', // remap link
1520
+ }}
1521
+ />
1522
+ ```
1523
+
1524
+ Register shortcuts programmatically via the engine:
1525
+
1526
+ ```jsx
1527
+ <RemyxEditor
1528
+ onReady={(engine) => {
1529
+ engine.keyboard.register('mod+shift+h', 'highlight');
1530
+
1531
+ // Get platform-specific label for display
1532
+ const label = engine.keyboard.getShortcutLabel('mod+b');
1533
+ // → '⌘B' on Mac, 'Ctrl+B' on Windows
1534
+ }}
1535
+ />
1536
+ ```
1537
+
1538
+ ## Heading Level Offset
1539
+
1540
+ When embedding the editor in a page that already has an `<h1>`, use `baseHeadingLevel` to offset heading levels so the editor's headings fit the page hierarchy:
1541
+
1542
+ ```jsx
1543
+ // Page has its own <h1>, so editor H1 renders as <h2>
1544
+ <h1>My Page Title</h1>
1545
+ <RemyxEditor baseHeadingLevel={2} />
1546
+ ```
1547
+
1548
+ | `baseHeadingLevel` | Editor H1 | Editor H2 | Editor H3 |
1549
+ |---|---|---|---|
1550
+ | `1` (default) | `<h1>` | `<h2>` | `<h3>` |
1551
+ | `2` | `<h2>` | `<h3>` | `<h4>` |
1552
+ | `3` | `<h3>` | `<h4>` | `<h5>` |
1553
+
1554
+ All heading levels are clamped to a maximum of `<h6>`.
1555
+
1556
+ ## Floating Toolbar & Context Menu
1557
+
1558
+ ### Floating Toolbar
1559
+
1560
+ A compact toolbar appears when text is selected, providing quick access to formatting:
1561
+
1562
+ ```jsx
1563
+ <RemyxEditor floatingToolbar={true} /> {/* enabled (default) */}
1564
+ <RemyxEditor floatingToolbar={false} /> {/* disabled */}
1565
+ ```
1566
+
1567
+ The floating toolbar shows bold, italic, underline, strikethrough, link, highlight, and heading options near the selection.
1568
+
1569
+ ### Context Menu
1570
+
1571
+ Right-click on the editor content to access a context menu with relevant actions:
1572
+
1573
+ ```jsx
1574
+ <RemyxEditor contextMenu={true} /> {/* enabled (default) */}
1575
+ <RemyxEditor contextMenu={false} /> {/* disabled */}
1576
+ ```
1577
+
1578
+ The context menu adapts to the clicked element — links show edit/remove options, images show resize options, tables show row/column operations.
1579
+
1580
+ ### Combining with Minimal Toolbar
1581
+
1582
+ For a clean writing experience, hide the main toolbar and rely on the floating toolbar and context menu:
1583
+
1584
+ ```jsx
1585
+ <RemyxEditor
1586
+ toolbar={[]}
1587
+ floatingToolbar={true}
1588
+ contextMenu={true}
1589
+ menuBar={false}
1590
+ statusBar={false}
1591
+ placeholder="Just start writing..."
1592
+ height={500}
1593
+ />
1594
+ ```
1595
+
1596
+ ## Command Palette
1597
+
1598
+ The command palette provides a searchable overlay listing all available editor commands. Open it via the toolbar button or `Mod+Shift+P` (`Cmd+Shift+P` on Mac, `Ctrl+Shift+P` on Windows/Linux).
1599
+
1600
+ ```jsx
1601
+ <RemyxEditor commandPalette={true} /> {/* enabled (default) */}
1602
+ <RemyxEditor commandPalette={false} /> {/* disabled */}
1603
+ ```
1604
+
1605
+ Commands are organized by category (Text, Lists, Media, Layout, Advanced, Commands) and support fuzzy search. The palette includes all built-in commands, any registered via the engine's command registry, and custom items added with `registerCommandItems()`.
1606
+
1607
+ **Recently-used commands** are automatically pinned to the top of the palette under a "Recent" category. The last 5 executed commands are tracked in `localStorage`. This works for both the command palette (Mod+Shift+P) and the inline slash command menu (Mod+/).
1608
+
1609
+ **Custom command items** can be registered from anywhere in your application:
1610
+
1611
+ ```jsx
1612
+ import { registerCommandItems, unregisterCommandItem } from '@remyxjs/react';
1613
+
1614
+ // Register custom commands that appear in the palette
1615
+ registerCommandItems({
1616
+ id: 'saveToCloud',
1617
+ label: 'Save to Cloud',
1618
+ description: 'Upload current document',
1619
+ icon: '☁️',
1620
+ keywords: ['save', 'cloud', 'upload'],
1621
+ category: 'Custom',
1622
+ action: (engine) => uploadToCloud(engine.getHTML()),
1623
+ });
1624
+
1625
+ // Clean up when no longer needed
1626
+ unregisterCommandItem('saveToCloud');
1627
+ ```
1628
+
1629
+ To add the command palette button to a custom toolbar:
1630
+
1631
+ ```jsx
1632
+ <RemyxEditor
1633
+ toolbar={[
1634
+ ['bold', 'italic', 'underline'],
1635
+ ['commandPalette'],
1636
+ ]}
1637
+ />
1638
+ ```
1639
+
1640
+ ## Autosave
1641
+
1642
+ Enable autosave with the `autosave` prop. Pass `true` for localStorage defaults, or an object for full control.
1643
+
1644
+ ### Basic (localStorage)
1645
+
1646
+ ```jsx
1647
+ <RemyxEditor value={content} onChange={setContent} autosave />
1648
+ ```
1649
+
1650
+ This saves to `localStorage` every 30 seconds and 2 seconds after each content change. A save-status indicator appears in the status bar (Saved / Saving... / Unsaved / Save failed), and a recovery banner appears on reload if unsaved content is detected.
1651
+
1652
+ ### Cloud Storage (AWS S3 / GCP / custom API)
1653
+
1654
+ ```jsx
1655
+ <RemyxEditor
1656
+ value={content}
1657
+ onChange={setContent}
1658
+ autosave={{
1659
+ provider: {
1660
+ endpoint: 'https://api.example.com/autosave',
1661
+ headers: { Authorization: `Bearer ${token}` },
1662
+ },
1663
+ key: 'doc-123',
1664
+ interval: 60000, // save every 60s
1665
+ debounce: 3000, // 3s after last edit
1666
+ }}
1667
+ />
1668
+ ```
1669
+
1670
+ For S3 presigned URLs or GCP signed URLs, use `buildUrl`:
1671
+
1672
+ ```jsx
1673
+ autosave={{
1674
+ provider: {
1675
+ endpoint: 'https://my-bucket.s3.amazonaws.com',
1676
+ buildUrl: (key) => getPresignedUrl(key),
1677
+ method: 'PUT',
1678
+ },
1679
+ key: `user-${userId}/draft`,
1680
+ }}
1681
+ ```
1682
+
1683
+ ### Filesystem (Electron / Tauri / Node)
1684
+
1685
+ ```jsx
1686
+ autosave={{
1687
+ provider: {
1688
+ writeFn: async (key, data) => window.electron.writeFile(`/saves/${key}.json`, data),
1689
+ readFn: async (key) => window.electron.readFile(`/saves/${key}.json`),
1690
+ deleteFn: async (key) => window.electron.deleteFile(`/saves/${key}.json`),
1691
+ },
1692
+ key: 'my-document',
1693
+ }}
1694
+ ```
1695
+
1696
+ ### Custom Provider
1697
+
1698
+ ```jsx
1699
+ autosave={{
1700
+ provider: {
1701
+ save: async (key, content, metadata) => { /* your save logic */ },
1702
+ load: async (key) => { /* return { content, timestamp } or null */ },
1703
+ clear: async (key) => { /* your delete logic */ },
1704
+ },
1705
+ }}
1706
+ ```
1707
+
1708
+ ### AutosaveConfig Options
1709
+
1710
+ | Option | Type | Default | Description |
1711
+ | --- | --- | --- | --- |
1712
+ | `enabled` | `boolean` | `true` | Toggle autosave on/off |
1713
+ | `interval` | `number` | `30000` | Periodic save interval in ms |
1714
+ | `debounce` | `number` | `2000` | Debounce delay after content change in ms |
1715
+ | `provider` | `string \| object` | `'localStorage'` | Storage provider config |
1716
+ | `key` | `string` | `'rmx-default'` | Storage key for this editor instance |
1717
+ | `onRecover` | `(data) => void` | — | Callback when recovery data is found |
1718
+ | `showRecoveryBanner` | `boolean` | `true` | Show the recovery banner UI |
1719
+ | `showSaveStatus` | `boolean` | `true` | Show save status in the status bar |
1720
+
1721
+ ### useAutosave Hook
1722
+
1723
+ For custom UIs, use the `useAutosave` hook directly:
1724
+
1725
+ ```jsx
1726
+ import { useAutosave } from '@remyxjs/react';
1727
+
1728
+ function MyEditor({ engine }) {
1729
+ const { saveStatus, lastSaved, recoveryData, recoverContent, dismissRecovery } =
1730
+ useAutosave(engine, { enabled: true, key: 'doc-123' });
1731
+
1732
+ return (
1733
+ <div>
1734
+ <span>Status: {saveStatus}</span>
1735
+ {recoveryData && (
1736
+ <div>
1737
+ <button onClick={recoverContent}>Restore</button>
1738
+ <button onClick={dismissRecovery}>Dismiss</button>
1739
+ </div>
1740
+ )}
1741
+ </div>
1742
+ );
1743
+ }
1744
+ ```
1745
+
1746
+ ## UX/UI Features
1747
+
1748
+ ### Empty State
1749
+
1750
+ Show a configurable empty state when the editor has no content:
1751
+ ```jsx
1752
+ // Default illustration + "Start typing..." message
1753
+ <RemyxEditor emptyState />
1754
+
1755
+ // Custom empty state component
1756
+ <RemyxEditor emptyState={<div>Drop content here or start typing...</div>} />
1757
+ ```
1758
+
1759
+ ### Breadcrumb Bar
1760
+
1761
+ Display the DOM path to the current cursor position:
1762
+ ```jsx
1763
+ <RemyxEditor breadcrumb />
1764
+ ```
1765
+ Shows a bar like "Blockquote > List > Item 2" or "Table > Row 3 > Cell 1" that updates on every selection change.
1766
+
1767
+ ### Minimap
1768
+
1769
+ Show a scaled-down document preview on the right edge:
1770
+ ```jsx
1771
+ <RemyxEditor minimap />
1772
+ ```
1773
+ Click anywhere on the minimap to scroll to that position. Updates automatically on content changes.
1774
+
1775
+ ### Split View
1776
+
1777
+ Side-by-side edit + preview pane:
1778
+ ```jsx
1779
+ <RemyxEditor splitViewFormat="html" /> // HTML preview
1780
+ <RemyxEditor splitViewFormat="markdown" /> // Markdown preview
1781
+ ```
1782
+ Toggle via `Mod+Shift+V` or the toolbar button. The preview updates in real time.
1783
+
1784
+ ### Distraction-Free Mode
1785
+
1786
+ Hide all editor chrome (toolbar, menu bar, status bar) for focused writing:
1787
+ ```jsx
1788
+ // Trigger via command
1789
+ engine.executeCommand('distractionFree');
1790
+ ```
1791
+ Shortcut: `Mod+Shift+D`. Chrome reappears on mouse movement and auto-hides after 3 seconds.
1792
+
1793
+ ### Toolbar Customization
1794
+
1795
+ Let users drag-and-drop toolbar buttons to rearrange them:
1796
+ ```jsx
1797
+ <RemyxEditor
1798
+ customizableToolbar
1799
+ onToolbarChange={(newOrder) => console.log('Toolbar reordered:', newOrder)}
1800
+ />
1801
+ ```
1802
+ The custom order is persisted in `localStorage` and restored on next load.
1803
+
1804
+ ### Typography Controls
1805
+
1806
+ Fine-tune line height, letter spacing, and paragraph spacing via the `typography` toolbar dropdown or programmatically:
1807
+ ```jsx
1808
+ // Add 'typography' to your toolbar config
1809
+ <RemyxEditor toolbar={[['bold', 'italic'], ['typography']]} />
1810
+ ```
1811
+
1812
+ ### Color Palette Presets
1813
+
1814
+ Save and reuse custom color palettes. A "Save Preset" button appears in the color picker dropdown. Presets are stored in `localStorage`.
1815
+
1816
+ ## Collaboration
1817
+
1818
+ ### useSpellcheck Hook
1819
+
1820
+ For reactive spellcheck/grammar state, use the `useSpellcheck` hook:
1821
+
1822
+ ```jsx
1823
+ import { useSpellcheck, SpellcheckPlugin } from '@remyxjs/react';
1824
+
1825
+ function MyEditor() {
1826
+ const engineRef = useRef(null);
1827
+ const {
1828
+ errors, // Array of current spellcheck/grammar errors
1829
+ stats, // { total, grammar, style, byRule }
1830
+ enabled, // boolean — whether spellcheck is active
1831
+ stylePreset, // current writing style preset
1832
+ language, // current BCP 47 language tag
1833
+ toggleSpellcheck, // () => boolean — toggle on/off
1834
+ checkGrammar, // () => Promise<Array> — run grammar check
1835
+ addToDictionary, // (word) => void
1836
+ ignoreWord, // (word) => void
1837
+ setWritingStyle, // ('formal'|'casual'|'technical'|'academic') => void
1838
+ setLanguage, // (lang) => void
1839
+ getDictionary, // () => string[]
1840
+ } = useSpellcheck(engineRef.current);
1841
+
1842
+ return (
1843
+ <div>
1844
+ <span>{stats.total} issues found</span>
1845
+ <button onClick={toggleSpellcheck}>{enabled ? 'Disable' : 'Enable'}</button>
1846
+ <RemyxEditor
1847
+ plugins={[SpellcheckPlugin({ stylePreset: 'formal' })]}
1848
+ onReady={(engine) => { engineRef.current = engine }}
1849
+ />
1850
+ </div>
1851
+ );
1852
+ }
1853
+ ```
1854
+
1855
+ ### useCollaboration Hook
1856
+
1857
+ For real-time collaborative editing, use the `useCollaboration` hook:
1858
+
1859
+ ```jsx
1860
+ import { useCollaboration, CollaborationBar, CollaborationPlugin } from '@remyxjs/react';
1861
+
1862
+ function CollaborativeEditor() {
1863
+ const engineRef = useRef(null);
1864
+ const { status, peers, connect, disconnect } = useCollaboration(engineRef.current);
1865
+
1866
+ return (
1867
+ <div>
1868
+ <CollaborationBar status={status} peers={peers} />
1869
+ <button onClick={() => connect({ serverUrl: 'wss://signal.example.com', roomId: 'room-1', userName: 'Alice' })}>
1870
+ Join
1871
+ </button>
1872
+ <button onClick={disconnect}>Leave</button>
1873
+ <RemyxEditor
1874
+ plugins={[CollaborationPlugin({ autoConnect: false })]}
1875
+ onReady={(engine) => { engineRef.current = engine; }}
1876
+ />
1877
+ </div>
1878
+ );
1879
+ }
1880
+ ```
1881
+
1882
+ #### useCollaboration return value
1883
+
1884
+ | Property | Type | Description |
1885
+ | --- | --- | --- |
1886
+ | `status` | `'connected' \| 'disconnected' \| 'connecting' \| 'error'` | Current connection state |
1887
+ | `peers` | `Array<{ id, name, color, cursor, isActive }>` | List of connected peers |
1888
+ | `connect` | `(opts: { serverUrl, roomId, userName }) => void` | Connect to a collaboration room |
1889
+ | `disconnect` | `() => void` | Disconnect from the room |
1890
+
1891
+ #### CollaborationBar props
1892
+
1893
+ | Prop | Type | Description |
1894
+ | --- | --- | --- |
1895
+ | `status` | `string` | Connection status (from `useCollaboration`) |
1896
+ | `peers` | `Array` | Peer list (from `useCollaboration`) |
1897
+ | `className` | `string` | Additional CSS class |
1898
+ | `showAvatars` | `boolean` | Show peer avatar circles (default: `true`) |
1899
+
1900
+ ## Form Integration
1901
+
1902
+ ### Next.js / Remix Form
1903
+
1904
+ ```jsx
1905
+ function ArticleForm() {
1906
+ const [content, setContent] = useState('');
1907
+
1908
+ return (
1909
+ <form method="post" action="/api/articles">
1910
+ <input type="text" name="title" placeholder="Title" />
1911
+ <RemyxEditor value={content} onChange={setContent} height={400} />
1912
+ <input type="hidden" name="body" value={content} />
1913
+ <button type="submit">Publish</button>
1914
+ </form>
1915
+ );
1916
+ }
1917
+ ```
1918
+
1919
+ ### Textarea Sync
1920
+
1921
+ Attach to a `<textarea>` for native form submission — the textarea value stays in sync automatically:
1922
+
1923
+ ```jsx
1924
+ function CommentForm() {
1925
+ const textareaRef = useRef(null);
1926
+
1927
+ return (
1928
+ <form onSubmit={handleSubmit}>
1929
+ <textarea ref={textareaRef} name="comment" hidden />
1930
+ <RemyxEditor
1931
+ attachTo={textareaRef}
1932
+ toolbar={[['bold', 'italic', 'link']]}
1933
+ statusBar={false}
1934
+ height={150}
1935
+ placeholder="Write a comment..."
1936
+ />
1937
+ <button type="submit">Post</button>
1938
+ </form>
1939
+ );
1940
+ }
1941
+ ```
1942
+
1943
+ ### Controlled with Validation
1944
+
1945
+ ```jsx
1946
+ function ValidatedEditor() {
1947
+ const [content, setContent] = useState('');
1948
+ const [error, setError] = useState('');
1949
+ const engineRef = useRef(null);
1950
+
1951
+ const validate = () => {
1952
+ const words = engineRef.current?.getWordCount() || 0;
1953
+ if (words < 10) {
1954
+ setError('Please write at least 10 words');
1955
+ return false;
1956
+ }
1957
+ if (words > 5000) {
1958
+ setError('Maximum 5000 words allowed');
1959
+ return false;
1960
+ }
1961
+ setError('');
1962
+ return true;
1963
+ };
1964
+
1965
+ return (
1966
+ <div>
1967
+ <RemyxEditor
1968
+ value={content}
1969
+ onChange={(html) => { setContent(html); setError(''); }}
1970
+ onReady={(engine) => { engineRef.current = engine }}
1971
+ onBlur={validate}
1972
+ />
1973
+ {error && <p style={{ color: 'red' }}>{error}</p>}
1974
+ <button onClick={() => validate() && submitForm(content)}>
1975
+ Submit
1976
+ </button>
1977
+ </div>
1978
+ );
1979
+ }
1980
+ ```
1981
+
1982
+ ## Exports
1983
+
1984
+ `@remyxjs/react` re-exports everything from `@remyxjs/core` for convenience. You only need one import source:
1985
+
1986
+ ```js
1987
+ // React components & hooks
1988
+ import { RemyxEditor, useRemyxEditor, useEditorEngine } from '@remyxjs/react';
1989
+ import { RemyxConfigProvider, useRemyxConfig } from '@remyxjs/react';
1990
+
1991
+ // External config
1992
+ import { RemyxEditorFromConfig, useExternalConfig } from '@remyxjs/react';
1993
+ import { loadConfig } from '@remyxjs/react'; // re-exported from core
1994
+
1995
+ // Toolbar
1996
+ import { TOOLBAR_PRESETS, DEFAULT_TOOLBAR, removeToolbarItems, addToolbarItems, createToolbar } from '@remyxjs/react';
1997
+
1998
+ // Defaults
1999
+ import { DEFAULT_FONTS, DEFAULT_FONT_SIZES, DEFAULT_COLORS, DEFAULT_KEYBINDINGS, DEFAULT_MENU_BAR } from '@remyxjs/react';
2000
+
2001
+ // Fonts
2002
+ import { removeFonts, addFonts, loadGoogleFonts } from '@remyxjs/react';
2003
+
2004
+ // Theming
2005
+ import { createTheme, THEME_VARIABLES, THEME_PRESETS } from '@remyxjs/react';
2006
+ import { createToolbarItemTheme, resolveToolbarItemStyle, TOOLBAR_ITEM_STYLE_KEYS } from '@remyxjs/react';
2007
+
2008
+ // Markdown & paste
2009
+ import { htmlToMarkdown, markdownToHtml, cleanPastedHTML, looksLikeMarkdown } from '@remyxjs/react';
2010
+
2011
+ // Document conversion
2012
+ import { convertDocument, isImportableFile, getSupportedExtensions, getSupportedFormatNames } from '@remyxjs/react';
2013
+
2014
+ // Export
2015
+ import { exportAsPDF, exportAsDocx, exportAsMarkdown } from '@remyxjs/react';
2016
+
2017
+ // Plugins
2018
+ import { createPlugin, WordCountPlugin, AutolinkPlugin, PlaceholderPlugin, SyntaxHighlightPlugin } from '@remyxjs/react';
2019
+
2020
+ // Syntax highlighting utilities
2021
+ import { SUPPORTED_LANGUAGES, LANGUAGE_MAP, detectLanguage, tokenize } from '@remyxjs/react';
2022
+
2023
+ // Command palette
2024
+ import { SLASH_COMMAND_ITEMS, filterSlashItems } from '@remyxjs/react';
2025
+
2026
+ // Advanced Link Management
2027
+ import { LinkPlugin, detectLinks, slugify } from '@remyxjs/react';
2028
+
2029
+ // Templates, Keyboard, Drag & Drop
2030
+ import { TemplatePlugin, renderTemplate, getTemplateLibrary } from '@remyxjs/react';
2031
+ import { KeyboardPlugin, getHeadings } from '@remyxjs/react';
2032
+ import { DragDropPlugin } from '@remyxjs/react';
2033
+
2034
+ // Math & Equations
2035
+ import { MathPlugin, getSymbolPalette, parseMathExpressions, latexToMathML } from '@remyxjs/react';
2036
+
2037
+ // Table of Contents
2038
+ import { TocPlugin, buildOutline, renderTocHTML, validateHeadingHierarchy } from '@remyxjs/react';
2039
+
2040
+ // Content Analytics
2041
+ import { AnalyticsPlugin, analyzeContent, keywordDensity, seoAnalysis } from '@remyxjs/react';
2042
+
2043
+ // Callouts & Alerts
2044
+ import { CalloutPlugin, registerCalloutType, getCalloutTypes, parseGFMAlert } from '@remyxjs/react';
2045
+
2046
+ // Comments & Annotations
2047
+ import { CommentsPlugin, parseMentions } from '@remyxjs/react'; // core exports
2048
+ import { useComments, CommentsPanel } from '@remyxjs/react'; // React-specific
2049
+
2050
+ // Spelling & Grammar
2051
+ import { SpellcheckPlugin, analyzeGrammar, STYLE_PRESETS } from '@remyxjs/react'; // core exports
2052
+ import { useSpellcheck } from '@remyxjs/react'; // React-specific
2053
+
2054
+ // Collaboration
2055
+ import { CollaborationPlugin } from '@remyxjs/react'; // core export
2056
+ import { useCollaboration, CollaborationBar } from '@remyxjs/react'; // React-specific
2057
+
2058
+ // Config
2059
+ import { defineConfig } from '@remyxjs/react';
2060
+
2061
+ // Core engine (advanced)
2062
+ import { EditorEngine, EventBus, CommandRegistry, Selection, History } from '@remyxjs/react';
2063
+ ```
2064
+
2065
+ ## TypeScript
2066
+
2067
+ This package ships with TypeScript declarations. Key types:
2068
+
2069
+ ```ts
2070
+ import type {
2071
+ RemyxEditorProps,
2072
+ MenuBarConfig,
2073
+ Plugin,
2074
+ UseRemyxEditorReturn,
2075
+ UseEditorEngineReturn,
2076
+ } from '@remyxjs/react';
2077
+ ```
2078
+
2079
+ ### RemyxEditorProps
2080
+
2081
+ Full type definition for all `<RemyxEditor>` props including `value`, `onChange`, `toolbar`, `menuBar`, `theme`, `plugins`, `uploadHandler`, and more.
2082
+
2083
+ ### EditorEngine
2084
+
2085
+ The core engine type with methods like `getHTML()`, `setHTML()`, `executeCommand()`, `on()`, `off()`, and `destroy()`.
2086
+
2087
+ ## Using `@remyxjs/core` Directly
2088
+
2089
+ For non-React integrations (Vue, Svelte, Angular, vanilla JS, or Node.js), use the core package directly:
2090
+
2091
+ ```bash
2092
+ npm install @remyxjs/core
2093
+ ```
2094
+
2095
+ ```js
2096
+ import { EditorEngine, EventBus } from '@remyxjs/core';
2097
+ import '@remyxjs/core/style.css';
2098
+
2099
+ // Create an editor on any DOM element
2100
+ const element = document.querySelector('#editor');
2101
+ const engine = new EditorEngine(element, { outputFormat: 'html' });
2102
+
2103
+ // Register commands
2104
+ import { registerFormattingCommands, registerListCommands } from '@remyxjs/core';
2105
+ registerFormattingCommands(engine);
2106
+ registerListCommands(engine);
2107
+
2108
+ // Initialize
2109
+ engine.init();
2110
+
2111
+ // Listen for changes
2112
+ engine.on('content:change', () => {
2113
+ console.log(engine.getHTML());
2114
+ });
2115
+
2116
+ // Cleanup
2117
+ engine.destroy();
2118
+ ```
2119
+
2120
+ `@remyxjs/core` exports 90+ named symbols — the full engine, all 17 command register functions, plugin system (including syntax highlighting), autosave providers, utilities (markdown, paste cleaning, export, fonts, themes, toolbar config), constants, and `defineConfig()`. See the [core package README](../remyx-core/README.md) for the full API.
2121
+
2122
+ ## Architecture
2123
+
2124
+ ```
2125
+ @remyxjs/react
2126
+ components/
2127
+ RemyxEditor.jsx Main editor component
2128
+ Toolbar/ Toolbar buttons, dropdowns, color pickers
2129
+ MenuBar/ Application-style menu system
2130
+ Modals/ Link, image, table, export, import modals
2131
+ EditArea/ Content area, floating toolbar, image resize
2132
+ StatusBar/ Word/character count
2133
+ ContextMenu/ Right-click menu
2134
+ hooks/
2135
+ useEditorEngine.js Low-level engine lifecycle hook
2136
+ useRemyxEditor.js High-level editor setup hook
2137
+ useSelection.js Selection state tracking
2138
+ useModal.js Modal open/close state
2139
+ useContextMenu.js Context menu positioning
2140
+ config/
2141
+ RemyxConfigProvider.jsx React context for shared config
2142
+ icons/
2143
+ index.jsx SVG icon components
2144
+ types/
2145
+ index.d.ts TypeScript declarations
2146
+ ```
2147
+
2148
+ ## Peer Dependencies
2149
+
2150
+ | Package | Version |
2151
+ | --- | --- |
2152
+ | `@remyxjs/core` | >= 0.34.0 |
2153
+ | `react` | >= 18.0.0 |
2154
+ | `react-dom` | >= 18.0.0 |
2155
+
2156
+ ## License
2157
+
2158
+ MIT