@editneo/react 0.1.0 → 0.1.1
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.
- package/README.md +293 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# @editneo/react
|
|
2
|
+
|
|
3
|
+
The React component layer for EditNeo. This package provides `NeoEditor` (the root editor component), a set of ready-made block renderers, interactive UI components like the floating toolbar and slash-command menu, and hooks for reading and manipulating editor state.
|
|
4
|
+
|
|
5
|
+
Rendering is virtualized with `@tanstack/react-virtual`, so documents with thousands of blocks remain responsive.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @editneo/react @editneo/core
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
React 18 or 19 is required as a peer dependency.
|
|
14
|
+
|
|
15
|
+
## Getting Started
|
|
16
|
+
|
|
17
|
+
A minimal working editor:
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import { NeoEditor } from "@editneo/react";
|
|
21
|
+
|
|
22
|
+
function App() {
|
|
23
|
+
return <NeoEditor id="my-document" />;
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The `id` prop is a unique identifier for the document. It is used to namespace IndexedDB storage and sync rooms.
|
|
28
|
+
|
|
29
|
+
## Components
|
|
30
|
+
|
|
31
|
+
### `<NeoEditor />`
|
|
32
|
+
|
|
33
|
+
The root component. It sets up the editor context, initializes the optional `SyncManager`, applies the theme, and renders the virtualized block canvas.
|
|
34
|
+
|
|
35
|
+
| Prop | Type | Default | Description |
|
|
36
|
+
| ------------- | ------------------------------------------------- | -------- | --------------------------------------------------------------- |
|
|
37
|
+
| `id` | `string` | required | Unique document identifier |
|
|
38
|
+
| `offline` | `boolean` | `true` | Enable offline persistence via IndexedDB |
|
|
39
|
+
| `syncConfig` | `{ url: string; room: string }` | — | WebSocket server URL and room name for real-time collaboration |
|
|
40
|
+
| `theme` | `{ mode: 'light' \| 'dark'; [key: string]: any }` | — | Theme configuration |
|
|
41
|
+
| `renderBlock` | `(block, defaultRender) => ReactNode` | — | Intercept rendering for custom block types |
|
|
42
|
+
| `className` | `string` | — | CSS class for the outer wrapper |
|
|
43
|
+
| `children` | `ReactNode` | — | Toolbar, menus, or other UI to render inside the editor context |
|
|
44
|
+
|
|
45
|
+
**Usage with collaboration:**
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
<NeoEditor
|
|
49
|
+
id="shared-doc"
|
|
50
|
+
syncConfig={{
|
|
51
|
+
url: "wss://your-yjs-server.com",
|
|
52
|
+
room: "shared-doc",
|
|
53
|
+
}}
|
|
54
|
+
theme={{ mode: "dark" }}
|
|
55
|
+
>
|
|
56
|
+
<Aeropeak />
|
|
57
|
+
<SlashMenu />
|
|
58
|
+
</NeoEditor>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Custom block rendering:**
|
|
62
|
+
|
|
63
|
+
If you have custom block types beyond the built-in ones, use `renderBlock` to intercept them:
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
<NeoEditor
|
|
67
|
+
id="doc"
|
|
68
|
+
renderBlock={(block, defaultRender) => {
|
|
69
|
+
if (block.type === "spreadsheet") {
|
|
70
|
+
return <SpreadsheetEmbed block={block} />;
|
|
71
|
+
}
|
|
72
|
+
return defaultRender;
|
|
73
|
+
}}
|
|
74
|
+
/>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### `<NeoCanvas />`
|
|
78
|
+
|
|
79
|
+
The virtualized document canvas. It renders only the blocks currently visible in the viewport, using `@tanstack/react-virtual` for smooth scrolling through large documents. You typically don't render this directly — `NeoEditor` includes it automatically.
|
|
80
|
+
|
|
81
|
+
### `<BlockRenderer />`
|
|
82
|
+
|
|
83
|
+
Routes each block to the correct renderer based on its `type` field. Supports all built-in block types:
|
|
84
|
+
|
|
85
|
+
- **Text blocks:** paragraph, heading-1, heading-2, heading-3
|
|
86
|
+
- **Lists:** bullet-list, ordered-list, todo-list
|
|
87
|
+
- **Media:** image, video
|
|
88
|
+
- **Structural:** quote, callout, divider, code-block
|
|
89
|
+
|
|
90
|
+
If `renderBlock` is provided through the editor context, it is called first, giving you the chance to handle custom types before falling through to the defaults.
|
|
91
|
+
|
|
92
|
+
### `<EditableBlock />`
|
|
93
|
+
|
|
94
|
+
Handles the content-editable rendering and input processing for a single block. It converts the block's `Span[]` content into styled inline elements (bold, italic, code, underline, strikethrough, links). It also handles:
|
|
95
|
+
|
|
96
|
+
- Enter key to split the block and create a new paragraph
|
|
97
|
+
- Backspace at the start of a block to delete it
|
|
98
|
+
- Input events to update the block's text content
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Interactive Components
|
|
103
|
+
|
|
104
|
+
### `<Aeropeak />` — Floating Toolbar
|
|
105
|
+
|
|
106
|
+
A toolbar that appears above the user's text selection. By default it shows Bold, Italic, Strikethrough, and Link buttons. You can replace the default buttons with your own by passing children.
|
|
107
|
+
|
|
108
|
+
| Prop | Type | Default | Description |
|
|
109
|
+
| ----------- | ----------------------------- | -------- | -------------------------------------------- |
|
|
110
|
+
| `children` | `ReactNode` | — | Custom toolbar content (replaces defaults) |
|
|
111
|
+
| `offset` | `number` | `10` | Vertical offset from the selection in pixels |
|
|
112
|
+
| `animation` | `'fade' \| 'scale' \| 'none'` | `'fade'` | Appearance animation |
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
// Default toolbar with Bold, Italic, Strike, Link
|
|
116
|
+
<Aeropeak />
|
|
117
|
+
|
|
118
|
+
// Custom toolbar
|
|
119
|
+
<Aeropeak offset={12} animation="scale">
|
|
120
|
+
<AeroButton
|
|
121
|
+
icon={<strong>B</strong>}
|
|
122
|
+
label="Bold"
|
|
123
|
+
onClick={(editor) => editor.toggleMark?.('bold')}
|
|
124
|
+
/>
|
|
125
|
+
<Separator />
|
|
126
|
+
<AeroButton
|
|
127
|
+
icon={<em>I</em>}
|
|
128
|
+
label="Italic"
|
|
129
|
+
onClick={(editor) => editor.toggleMark?.('italic')}
|
|
130
|
+
/>
|
|
131
|
+
</Aeropeak>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**Compound components:**
|
|
135
|
+
|
|
136
|
+
- `Aeropeak.Bold` / `Aeropeak.Italic` / `Aeropeak.Strike` / `Aeropeak.Link` — prebuilt buttons
|
|
137
|
+
- `AeroButton` — a button that receives the editor instance when clicked
|
|
138
|
+
- `Separator` — a thin vertical divider between button groups
|
|
139
|
+
|
|
140
|
+
### `<SlashMenu />` — Command Palette
|
|
141
|
+
|
|
142
|
+
Appears when the user types `/` in the editor. Lists available block types, supports keyboard navigation (arrow keys + enter), and filters results as the user continues typing.
|
|
143
|
+
|
|
144
|
+
| Prop | Type | Default | Description |
|
|
145
|
+
| ---------------- | ------------------------------- | ------- | ------------------------------------------------------ |
|
|
146
|
+
| `customCommands` | `CommandItem[]` | `[]` | Additional commands to show alongside the defaults |
|
|
147
|
+
| `filter` | `(cmd: CommandItem) => boolean` | — | Filter function to hide certain commands |
|
|
148
|
+
| `menuComponent` | `React.ComponentType` | — | Completely replace the menu UI with a custom component |
|
|
149
|
+
|
|
150
|
+
**Built-in commands:** Paragraph, Heading 1-3, Bulleted List, Ordered List, To-do List, Quote, Code Block, Divider, Callout, Image.
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
<SlashMenu
|
|
154
|
+
customCommands={[
|
|
155
|
+
{
|
|
156
|
+
key: "diagram",
|
|
157
|
+
label: "Diagram",
|
|
158
|
+
icon: <span>chart</span>,
|
|
159
|
+
execute: (editor) => editor.addBlock("image"),
|
|
160
|
+
},
|
|
161
|
+
]}
|
|
162
|
+
/>
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**`CommandItem` interface:**
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
interface CommandItem {
|
|
169
|
+
key: string; // Unique key
|
|
170
|
+
label: string; // Display label
|
|
171
|
+
icon?: ReactNode; // Icon element
|
|
172
|
+
execute: (editor: any) => void; // Action to perform
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### `<PDFDropZone />`
|
|
177
|
+
|
|
178
|
+
A wrapper component that detects PDF file drops and runs client-side extraction to convert the PDF into editor blocks.
|
|
179
|
+
|
|
180
|
+
| Prop | Type | Default | Description |
|
|
181
|
+
| --------------- | ------------------------------------------- | ------- | ------------------------------------------------------------------------------------ |
|
|
182
|
+
| `onDrop` | `(files: File[]) => void` | — | Callback when files are dropped. If provided, the default PDF extraction is skipped. |
|
|
183
|
+
| `renderOverlay` | `(props: { isOver: boolean }) => ReactNode` | — | Custom overlay content shown during drag-over |
|
|
184
|
+
| `children` | `ReactNode` | — | Content inside the drop zone |
|
|
185
|
+
|
|
186
|
+
```tsx
|
|
187
|
+
<PDFDropZone>{/* Your editor content goes inside */}</PDFDropZone>
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### `<CursorOverlay />`
|
|
191
|
+
|
|
192
|
+
Displays colored cursors and labels for remote collaborators when using real-time sync. Each user's cursor position is tracked through Yjs awareness.
|
|
193
|
+
|
|
194
|
+
```tsx
|
|
195
|
+
<NeoEditor id="doc" syncConfig={{ url: "wss://...", room: "doc" }}>
|
|
196
|
+
<CursorOverlay />
|
|
197
|
+
</NeoEditor>
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Cursor colors are assigned automatically based on the user's awareness client ID.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Hooks
|
|
205
|
+
|
|
206
|
+
### `useEditor()`
|
|
207
|
+
|
|
208
|
+
The primary hook for interacting with the editor. Must be called inside a `<NeoEditor />`. Returns all store state and actions, plus convenience aliases.
|
|
209
|
+
|
|
210
|
+
```tsx
|
|
211
|
+
const {
|
|
212
|
+
blocks, // Record<string, NeoBlock>
|
|
213
|
+
rootBlocks, // string[]
|
|
214
|
+
selection, // { blockId, startOffset, endOffset }
|
|
215
|
+
addBlock, // (type, afterId?) => void
|
|
216
|
+
insertBlock, // alias for addBlock
|
|
217
|
+
updateBlock, // (id, partial) => void
|
|
218
|
+
deleteBlock, // (id) => void
|
|
219
|
+
toggleMark, // (mark) => void
|
|
220
|
+
undo, // () => void
|
|
221
|
+
redo, // () => void
|
|
222
|
+
} = useEditor();
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Throws an error if called outside of `<NeoEditor />`.
|
|
226
|
+
|
|
227
|
+
### `useSelection()`
|
|
228
|
+
|
|
229
|
+
A focused hook that subscribes only to the selection state, minimizing re-renders in components that don't care about the full document.
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
const selection = useSelection();
|
|
233
|
+
// { blockId: string | null, startOffset: number, endOffset: number }
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### `useSyncStatus()`
|
|
237
|
+
|
|
238
|
+
Returns the current sync connection status.
|
|
239
|
+
|
|
240
|
+
```tsx
|
|
241
|
+
const status = useSyncStatus();
|
|
242
|
+
// 'connected' or 'disconnected'
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Theming
|
|
248
|
+
|
|
249
|
+
`NeoEditor` applies a set of CSS variables to the document root. You can override them globally in your stylesheet:
|
|
250
|
+
|
|
251
|
+
```css
|
|
252
|
+
:root {
|
|
253
|
+
--neo-font-family: "Inter", system-ui, sans-serif;
|
|
254
|
+
--neo-font-size-body: 16px;
|
|
255
|
+
--neo-accent-color: #3b82f6;
|
|
256
|
+
--neo-bg-canvas: #ffffff;
|
|
257
|
+
--neo-text-primary: #111827;
|
|
258
|
+
--neo-border-radius: 4px;
|
|
259
|
+
--neo-block-spacing: 4px;
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Or switch between light and dark mode via the `theme` prop:
|
|
264
|
+
|
|
265
|
+
```tsx
|
|
266
|
+
<NeoEditor id="doc" theme={{ mode: "dark" }} />
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Dark mode sets `--neo-bg-canvas` to `#0f172a` and `--neo-text-primary` to `#f3f4f6`.
|
|
270
|
+
|
|
271
|
+
## Exports
|
|
272
|
+
|
|
273
|
+
Everything is exported from the package root:
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
// Components
|
|
277
|
+
export { NeoEditor, EditorContext } from "./NeoEditor";
|
|
278
|
+
export { NeoCanvas } from "./NeoCanvas";
|
|
279
|
+
export { EditableBlock } from "./EditableBlock";
|
|
280
|
+
|
|
281
|
+
// Interactive UI
|
|
282
|
+
export { PDFDropZone } from "./components/PDFDropZone";
|
|
283
|
+
export { CursorOverlay } from "./components/CursorOverlay";
|
|
284
|
+
export { Aeropeak, AeroButton, Separator } from "./components/Aeropeak";
|
|
285
|
+
export { SlashMenu } from "./components/SlashMenu";
|
|
286
|
+
|
|
287
|
+
// Hooks
|
|
288
|
+
export { useEditor, useSelection, useSyncStatus } from "./hooks";
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## License
|
|
292
|
+
|
|
293
|
+
MIT
|