@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.
Files changed (2) hide show
  1. package/README.md +293 -0
  2. 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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@editneo/react",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "React components for EditNeo — NeoEditor, Aeropeak, SlashMenu, and more",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",