@datalayer/lexical-loro 0.0.1 → 0.0.2

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 CHANGED
@@ -2,550 +2,667 @@
2
2
 
3
3
  [![Become a Sponsor](https://img.shields.io/static/v1?label=Become%20a%20Sponsor&message=%E2%9D%A4&logo=GitHub&style=flat&color=1ABC9C)](https://github.com/sponsors/datalayer)
4
4
 
5
- # Collaborative Plugin for Lexical based on Loro CRDT
5
+ # ✍️ 🦜 Lexical Loro - Collaborative Plugin for Lexical with Loro CRDT
6
6
 
7
- A real-time collaborative editing application for [Lexical](https://github.com/facebook/lexical) built with [Loro CRDT](https://github.com/loro-dev), React, TypeScript, Vite with a Python WebSocket server using [loro-py](https://github.com/loro-dev/loro-py) to maintain the Lexical JSON model sever-side
7
+ A collaborative editing plugin for [Lexical](https://github.com/facebook/lexical) Rich Editor built with [Loro](https://github.com/loro-dev) CRDT, providing real-time collaborative editing capabilities with conflict-free synchronization.
8
8
 
9
- Features both simple text editing and rich text editing with Lexical. Multiple users can edit the same documents simultaneously with conflict-free collaborative editing powered by Conflict-free Replicated Data Types (CRDTs).
9
+ ## Core Components
10
10
 
11
- **DISCLAIMER** Collaborative Cursors still need fixes, see [this issue](https://github.com/datalayer/lexical-loro/issues/1).
11
+ This package provides two main components for building collaborative text editors:
12
+
13
+ 1. **`LoroCollaborativePlugin.tsx`** - A Lexical plugin that integrates Loro CRDT for real-time collaborative editing
14
+ 2. **`lexical-loro` Python package** - A WebSocket server using [loro-py](https://github.com/loro-dev/loro-py) for maintaining document state server-side
15
+
16
+ ## Quick Start
17
+
18
+ ### Using the Lexical Plugin
19
+
20
+ ```tsx
21
+ import { LoroCollaborativePlugin } from './src/LoroCollaborativePlugin';
22
+
23
+ function MyEditor() {
24
+ return (
25
+ <LexicalComposer initialConfig={editorConfig}>
26
+ <RichTextPlugin />
27
+ <LoroCollaborativePlugin
28
+ websocketUrl="ws://localhost:8081"
29
+ docId="my-document"
30
+ username="user1"
31
+ />
32
+ </LexicalComposer>
33
+ );
34
+ }
35
+ ```
36
+
37
+ ### Using the Python Server
38
+
39
+ ```bash
40
+ # Install the Python package
41
+ pip install -e .
42
+
43
+ # Start the server
44
+ lexical-loro-server --port 8081
45
+ ```
12
46
 
13
- **NEW** Now supports both Node.js and Python WebSocket servers!
47
+ ## Examples
48
+
49
+ For complete working examples, see the `src/examples/` directory which contains:
50
+ - Full React application with dual editor support
51
+ - Server selection interface
52
+ - Connection status indicators
53
+ - Rich text formatting examples
54
+
55
+ **DISCLAIMER** Collaborative Cursors still need fixes, see [this issue](https://github.com/datalayer/lexical-loro/issues/1).
14
56
 
15
57
  <div align="center" style="text-align: center">
16
58
  <img alt="" src="https://assets.datalayer.tech/lexical-loro.gif" />
17
59
  </div>
18
60
 
19
- ## Features
61
+ ## Core Features
20
62
 
21
63
  - 🔄 **Real-time Collaboration**: Multiple users can edit the same document simultaneously
22
- - 🚀 **Conflict-free**: Uses Loro CRDT to automatically resolve conflicts
23
- - 📝 **Dual Editor Support**: Choose between simple text area or rich text Lexical editor
24
- - 🌐 **Multi-server Support**: Choose between Node.js and Python WebSocket servers
25
- - **Fast Development**: Built with Vite for lightning-fast development
26
- - 🎨 **Responsive Design**: Works on desktop and mobile devices
27
- - 📡 **Connection Status**: Visual indicators for connection state
28
- - ✨ **Rich Text Features**: Bold, italic, underline with real-time formatting sync
29
- - 🔧 **Server Selection**: Switch between Node.js and Python backends
64
+ - 🚀 **Conflict-free**: Uses Loro CRDT to automatically resolve conflicts
65
+ - 📝 **Lexical Integration**: Seamless integration with Lexical rich text editor
66
+ - 🌐 **WebSocket Server**: Python server for maintaining document state
67
+ - 📡 **Connection Management**: Robust WebSocket connection handling
68
+ - **Rich Text Support**: Preserves formatting during collaborative editing
69
+ - 🔧 **Extensible**: Plugin-based architecture for easy customization
30
70
 
31
71
  ## Technology Stack
32
72
 
33
- - **Frontend**: React 19 + TypeScript + Vite
34
- - **CRDT Library**: Loro CRDT
35
- - **Rich Text Editor**: Lexical (Facebook's extensible text editor)
36
- - **Backend Options**:
37
- - Node.js + TypeScript + ws library
38
- - Python + loro-py + websockets library
39
- - **Real-time Communication**: WebSockets (ws)
40
- - **Styling**: CSS3 with responsive design
41
- - **Development Tools**: ESLint, tsx, concurrently
42
-
43
- ## Getting Started
73
+ **Core Dependencies:**
74
+ - **Lexical**: v0.33.1 (Facebook's extensible text editor framework)
75
+ - **Loro CRDT**: v1.5.10 (Conflict-free replicated data types)
76
+ - **React**: 18/19 (for plugin hooks and components)
77
+ - **Python**: 3.8+ with loro-py and websockets
44
78
 
45
- ### Prerequisites
79
+ **Development Dependencies:**
80
+ - **TypeScript**: For type safety
81
+ - **Vite**: For building and development (examples only)
82
+ - **pytest**: Python testing
83
+ - **ESLint**: Code linting
46
84
 
47
- - Node.js (v16 or higher)
48
- - npm or yarn
49
- - Python 3.8+ (for Python server option)
50
- - pip3 (for Python dependencies)
51
-
52
- ### Installation
53
-
54
- 1. Install Node.js dependencies:
55
- ```bash
56
- npm install
57
- ```
85
+ ## Installation
58
86
 
59
- 2. Install Python dependencies (optional - for Python server):
60
- ```bash
61
- pip3 install -r requirements.txt
62
- # or run the setup script
63
- ./setup-python.sh
64
- ```
87
+ ### Core Plugin
65
88
 
66
- ### Running the Application
89
+ The Lexical plugin is a single TypeScript/React component that you can copy into your project:
67
90
 
68
- #### Option 1: All Servers (Recommended)
69
91
  ```bash
70
- npm run dev:all
92
+ # Copy the plugin file
93
+ cp src/LoroCollaborativePlugin.tsx your-project/src/
71
94
  ```
72
- This starts **both** WebSocket servers (Node.js on port 8080 and Python on port 8081) plus the React development server (port 5173). You can then switch between servers using the UI.
73
95
 
74
- #### Option 2: Python Server Only
96
+ **Dependencies required:**
75
97
  ```bash
76
- npm run dev:all:py
98
+ npm install lexical @lexical/react @lexical/selection loro-crdt react react-dom
77
99
  ```
78
- This starts only the Python WebSocket server (port 8081) and React development server.
79
100
 
80
- #### Option 3: Node.js Server Only
81
- ```bash
82
- npm run dev:all:js
83
- ```
84
- This starts only the Node.js WebSocket server (port 8080) and React development server.
101
+ ### Python Server
85
102
 
86
- #### Option 4: Run servers separately
103
+ Install the Python WebSocket server:
87
104
 
88
- **All servers manually:**
89
105
  ```bash
90
- # Terminal 1: Start Node.js WebSocket server
91
- npm run server
92
-
93
- # Terminal 2: Start Python WebSocket server
94
- npm run server:py
106
+ # Install from this repository
107
+ pip install -e .
95
108
 
96
- # Terminal 3: Start React development server
97
- npm run dev
109
+ # Or install specific dependencies
110
+ pip install websockets click loro
98
111
  ```
99
112
 
100
- **Node.js Server only:**
101
- ```bash
102
- # Terminal 1: Start Node.js WebSocket server
103
- npm run server
113
+ ## Usage
104
114
 
105
- # Terminal 2: Start React development server
106
- npm run dev
115
+ ### 1. Lexical Plugin Integration
116
+
117
+ Add the plugin to your Lexical editor:
118
+
119
+ ```tsx
120
+ import { LexicalComposer } from '@lexical/react/LexicalComposer';
121
+ import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
122
+ import { ContentEditable } from '@lexical/react/LexicalContentEditable';
123
+ import { LoroCollaborativePlugin } from './LoroCollaborativePlugin';
124
+
125
+ const editorConfig = {
126
+ namespace: 'MyEditor',
127
+ theme: {},
128
+ onError: console.error,
129
+ };
130
+
131
+ function CollaborativeEditor() {
132
+ return (
133
+ <LexicalComposer initialConfig={editorConfig}>
134
+ <div className="editor-container">
135
+ <RichTextPlugin
136
+ contentEditable={<ContentEditable className="editor-input" />}
137
+ placeholder={<div className="editor-placeholder">Start typing...</div>}
138
+ ErrorBoundary={() => <div>Error occurred</div>}
139
+ />
140
+ <LoroCollaborativePlugin
141
+ websocketUrl="ws://localhost:8081"
142
+ docId="shared-document"
143
+ username="user123"
144
+ />
145
+ </div>
146
+ </LexicalComposer>
147
+ );
148
+ }
107
149
  ```
108
150
 
109
- **Python Server only:**
110
- ```bash
111
- # Terminal 1: Start Python WebSocket server
112
- npm run server:py
113
- # or directly: python3 server.py
114
-
115
- # Terminal 2: Start React development server
116
- npm run dev
117
- ```
118
-
119
- 2. In another terminal, start the React development server:
120
- ```bash
121
- npm run dev
122
- ```
123
-
124
- ### Usage
125
-
126
- 1. Open your browser and navigate to the development server URL (typically `http://localhost:5173`)
127
- 2. **Select Server Type**: Use the server selection radio buttons to choose:
128
- - **Node.js Server**: `ws://localhost:8080` (TypeScript implementation)
129
- - **Python Server**: `ws://localhost:8081` (Python + loro-py implementation)
130
-
131
- 💡 **Tip**: When using `npm run dev:all`, both servers are running simultaneously, so you can switch between them in real-time!
132
-
133
- 3. **Choose Editor Type**: Click the tabs to select:
134
- - **Simple Text Editor**: A basic textarea for plain text collaboration
135
- - **Rich Text Editor (Lexical)**: A full-featured rich text editor with Bold/Italic/Underline formatting
136
- 4. Start typing in either editor
137
- 5. Open another browser window/tab or share the URL with others
138
- 6. All users will see real-time updates as they type in the same editor type
139
- 7. Each editor maintains its own document state (they are separate collaborative spaces)
140
-
141
- **Note**: You must disconnect from the current server before switching to a different server type.
142
-
143
- ### Testing Collaboration
144
-
145
- To test the real-time collaboration:
146
-
147
- 1. Open multiple browser tabs/windows to the development server URL
148
- 2. **Select the same server** in all tabs (Node.js or Python)
149
- 3. **Test Simple Text Editor**:
150
- - Keep all tabs on the "Simple Text Editor" tab
151
- - Start typing in one window - you'll see the changes appear in other windows instantly
152
- 4. **Test Lexical Rich Text Editor**:
153
- - Switch all tabs to the "Rich Text Editor (Lexical)" tab
154
- - Try formatting text with the toolbar buttons (Bold, Italic, Underline)
155
- - Changes and formatting will sync in real-time across all tabs
156
- 5. **Test Cross-Server Compatibility**:
157
- - Verify that documents are properly synchronized between Node.js and Python servers
158
- - Each server maintains its own document state
159
- 6. **Test Independent Documents**:
160
- - Have some tabs on "Simple Text Editor" and others on "Lexical Editor"
161
- - Notice that each editor type maintains its own separate document
162
- 5. **New collaborators will automatically receive the current document content** when they join
163
-
164
- **Note**: The application now properly synchronizes initial content:
165
- - When a new collaborator joins, they automatically receive the current document state for both editors
166
- - If no snapshot is available on the server, existing clients will provide their current state
167
- - The first client to join with content will automatically share their document state
168
- - Each editor type (simple text vs Lexical) maintains separate collaborative documents
151
+ ### 2. Python Server Setup
169
152
 
170
- ## Project Structure
153
+ Start the WebSocket server:
171
154
 
172
- ```
173
- src/
174
- ├── App.tsx # Main application component with tabbed interface
175
- ├── App.css # Application styles
176
- ├── CollaborativeEditor.tsx # Simple text editor component with Loro CRDT integration
177
- ├── CollaborativeEditor.css # Simple editor styles
178
- ├── LexicalCollaborativeEditor.tsx # Lexical rich text editor component
179
- ├── LexicalCollaborativeEditor.css # Lexical editor styles
180
- ├── LoroCollaborativePlugin.tsx # Lexical plugin for Loro CRDT integration
181
- ├── main.tsx # React application entry point
182
- └── vite-env.d.ts # Vite type definitions
155
+ ```bash
156
+ # Default port (8081)
157
+ lexical-loro-server
183
158
 
184
- server.ts # WebSocket server for real-time communication
185
- package.json # Dependencies and scripts
186
- ```
159
+ # Custom port
160
+ lexical-loro-server --port 8082
187
161
 
188
- ## How It Works
162
+ # With debug logging
163
+ lexical-loro-server --port 8081 --log-level DEBUG
164
+ ```
189
165
 
190
- ### Loro CRDT Integration
166
+ ### 3. Programmatic Server Usage
191
167
 
192
- The application uses Loro CRDT to manage collaborative editing across two different editor types:
168
+ ```python
169
+ import asyncio
170
+ from lexical_loro import LoroWebSocketServer
193
171
 
194
- 1. **Document Creation**: Each editor type creates its own Loro document with a unique identifier:
195
- - Simple Text Editor: `shared-text`
196
- - Lexical Editor: `lexical-shared-doc`
197
- 2. **Local Changes**: When a user types, changes are applied to the corresponding local Loro document
198
- 3. **Change Detection**: The application detects insertions, deletions, and replacements
199
- 4. **Synchronization**: Changes are serialized and sent to other clients via WebSocket with document ID
200
- 5. **Conflict Resolution**: Loro CRDT automatically merges changes without conflicts
172
+ async def main():
173
+ server = LoroWebSocketServer(port=8081)
174
+ await server.start()
175
+ print("Server running on ws://localhost:8081")
201
176
 
202
- The Complete Flow Diagram
177
+ if __name__ == "__main__":
178
+ asyncio.run(main())
179
+ ```
203
180
 
181
+ ## Plugin API
204
182
 
205
- Remote User Types
206
-
207
- WebSocket Message
208
-
209
- loro-update received
210
-
211
- loroDocRef.current.import(update)
212
-
213
- doc.subscribe() callback fires
214
-
215
- updateLexicalFromLoro(editor, newText)
216
-
217
- editor.update() with new content
218
-
219
- Lexical State Updated
220
-
221
- UI Re-renders with New Content
183
+ ### LoroCollaborativePlugin Props
222
184
 
223
- Protection Against Infinite Loops
185
+ ```tsx
186
+ interface LoroCollaborativePluginProps {
187
+ websocketUrl: string; // WebSocket server URL
188
+ docId: string; // Unique document identifier
189
+ username: string; // User identifier
190
+ userColor?: string; // User cursor color (optional)
191
+ debug?: boolean; // Enable debug logging (optional)
192
+ }
193
+ ```
224
194
 
225
- The system uses several mechanisms to prevent loops:
195
+ ### Plugin Features
196
+
197
+ - **Real-time Sync**: Automatically syncs all text changes via Loro CRDT
198
+ - **Cursor Tracking**: Shows other users' cursor positions (experimental)
199
+ - **Connection Management**: Handles reconnection and error states
200
+ - **Rich Text Preservation**: Maintains formatting during collaborative edits
201
+ - **Conflict Resolution**: Automatic conflict-free merging via CRDT
202
+
203
+
204
+ ## Initialization Best Practices
205
+
206
+ ⚠️ **Important**: To avoid race conditions and initial state corruption, always wait for collaboration initialization to complete before enabling other Lexical plugins or performing document operations.
207
+
208
+ ### Why Initialization Matters
209
+
210
+ Collaborative editing involves complex synchronization between:
211
+ - Local Lexical editor state
212
+ - Remote CRDT document state
213
+ - WebSocket connection establishment
214
+ - Initial document snapshot loading
215
+
216
+ Enabling other plugins or performing operations before this synchronization completes can cause:
217
+ - Document state corruption
218
+ - Lost edits
219
+ - Inconsistent collaborative state
220
+ - Race conditions between local and remote changes
221
+
222
+ ### Proper Plugin Ordering
223
+
224
+ ```tsx
225
+ function MyEditor() {
226
+ const [isCollabInitialized, setIsCollabInitialized] = useState(false);
227
+
228
+ return (
229
+ <LexicalComposer initialConfig={editorConfig}>
230
+ <div>
231
+ {/* ALWAYS load collaborative plugin first */}
232
+ <LoroCollaborativePlugin
233
+ websocketUrl="ws://localhost:8081"
234
+ docId="my-document"
235
+ onInitialization={(success) => {
236
+ setIsCollabInitialized(success);
237
+ console.log('Collaboration ready:', success);
238
+ }}
239
+ />
240
+
241
+ {/* WAIT for collaboration before enabling other plugins */}
242
+ {isCollabInitialized && (
243
+ <>
244
+ <HistoryPlugin />
245
+ <AutoLinkPlugin />
246
+ <ListPlugin />
247
+ <CheckListPlugin />
248
+ {/* Other plugins... */}
249
+ </>
250
+ )}
251
+
252
+ <RichTextPlugin
253
+ contentEditable={<ContentEditable />}
254
+ placeholder={<div>Loading collaborative editor...</div>}
255
+ ErrorBoundary={LexicalErrorBoundary}
256
+ />
257
+ </div>
258
+ </LexicalComposer>
259
+ );
260
+ }
261
+ ```
226
262
 
227
- isLocalChange.current flag - Prevents local changes from triggering remote updates
228
- { tag: 'collaboration' } on editor.update() - Allows the update listener to ignore these changes
229
- JSON comparison in updateLexicalFromLoro to avoid redundant updates
263
+ ### Initialization Callback
264
+
265
+ The `onInitialization` callback provides essential feedback:
266
+
267
+ ```tsx
268
+ <LoroCollaborativePlugin
269
+ websocketUrl="ws://localhost:8081"
270
+ docId="document-123"
271
+ onInitialization={(success: boolean) => {
272
+ if (success) {
273
+ // ✅ Safe to enable other plugins and features
274
+ console.log('Collaboration initialized successfully');
275
+ enableOtherFeatures();
276
+ } else {
277
+ // ❌ Handle initialization failure
278
+ console.error('Collaboration failed to initialize');
279
+ showErrorMessage('Failed to connect to collaborative server');
280
+ }
281
+ }}
282
+ />
283
+ ```
230
284
 
231
- When a Loro update is received, the Lexical state is updated through:
285
+ ### Visual Status Indicators
286
+
287
+ Provide users with clear feedback about initialization status:
288
+
289
+ ```tsx
290
+ function CollaborativeEditor() {
291
+ const [isInitialized, setIsInitialized] = useState(false);
292
+
293
+ return (
294
+ <div>
295
+ <div className="status-bar">
296
+ Collaboration: {isInitialized ? '✅ Ready' : '⏳ Connecting...'}
297
+ </div>
298
+
299
+ <LexicalComposer initialConfig={editorConfig}>
300
+ <LoroCollaborativePlugin
301
+ websocketUrl="ws://localhost:8081"
302
+ docId="document-123"
303
+ onInitialization={setIsInitialized}
304
+ />
305
+
306
+ {/* Editor becomes fully functional only after initialization */}
307
+ <RichTextPlugin
308
+ contentEditable={
309
+ <ContentEditable
310
+ style={{
311
+ opacity: isInitialized ? 1 : 0.5,
312
+ pointerEvents: isInitialized ? 'auto' : 'none'
313
+ }}
314
+ />
315
+ }
316
+ placeholder={
317
+ <div>
318
+ {isInitialized
319
+ ? 'Start typing...'
320
+ : 'Connecting to collaboration server...'
321
+ }
322
+ </div>
323
+ }
324
+ ErrorBoundary={LexicalErrorBoundary}
325
+ />
326
+ </LexicalComposer>
327
+ </div>
328
+ );
329
+ }
330
+ ```
232
331
 
233
- WebSocket receives loro-update message
332
+ ### Common Anti-Patterns to Avoid
234
333
 
235
- loroDocRef.current.import(update) applies the change to Loro
236
- doc.subscribe() callback automatically fires
237
- updateLexicalFromLoro() converts Loro text to Lexical state
238
- editor.setEditorState() or DOM manipulation updates the editor
334
+ **Don't** enable plugins immediately:
335
+ ```tsx
336
+ // WRONG: Race condition risk
337
+ <LoroCollaborativePlugin websocketUrl="..." />
338
+ <HistoryPlugin /> {/* May interfere with initial sync */}
339
+ ```
239
340
 
240
- The bridge is the doc.subscribe() callback on line 1901 - this is what makes Lexical automatically reflect any Loro document changes!
341
+ **Don't** perform immediate document operations:
342
+ ```tsx
343
+ // WRONG: May overwrite remote content
344
+ useEffect(() => {
345
+ editor.update(() => {
346
+ $getRoot().clear(); // Dangerous before sync!
347
+ });
348
+ }, []);
349
+ ```
241
350
 
242
- ### Lexical Integration
351
+ **Don't** ignore initialization status:
352
+ ```tsx
353
+ // WRONG: No feedback on connection issues
354
+ <LoroCollaborativePlugin websocketUrl="..." />
355
+ ```
243
356
 
244
- The Lexical editor integration includes:
357
+ ### Debugging Initialization Issues
245
358
 
246
- 1. **LoroCollaborativePlugin**: A custom Lexical plugin that bridges Lexical and Loro CRDT
247
- 2. **Bidirectional Sync**: Changes flow from Lexical → Loro → WebSocket and vice versa
248
- 3. **Rich Text Preservation**: The plugin maintains rich text formatting during collaborative editing
249
- 4. **Independent State**: Lexical editor maintains separate document state from simple text editor
359
+ If initialization fails, check:
250
360
 
251
- ### WebSocket Communication
361
+ 1. **WebSocket Connection**: Ensure server is running and accessible
362
+ 2. **Network Issues**: Check browser network tab for connection errors
363
+ 3. **CORS Settings**: Verify server allows cross-origin WebSocket connections
364
+ 4. **Document ID**: Ensure unique document IDs for different documents
365
+ 5. **Server Logs**: Enable debug logging on server side
252
366
 
253
- The WebSocket server:
254
- - Maintains connections to all clients
255
- - Broadcasts Loro document updates to all connected clients with document ID filtering
256
- - Handles client connections and disconnections
257
- - Provides connection status feedback
258
- - Stores separate snapshots for each document type
367
+ ```bash
368
+ # Enable debug logging
369
+ export LEXICAL_LORO_LOG_LEVEL=DEBUG
370
+ lexical-loro-server
371
+ ```
259
372
 
260
- ### Real-time Updates
373
+ ## Server API
261
374
 
262
- 1. User types in the text area
263
- 2. Change is applied to local Loro document
264
- 3. Document update is serialized and sent via WebSocket
265
- 4. Other clients receive the update and apply it to their documents
266
- 5. UI is updated to reflect the changes
375
+ ### LoroWebSocketServer Class
267
376
 
268
- ### Initial Content Synchronization
377
+ ```python
378
+ from lexical_loro import LoroWebSocketServer
269
379
 
270
- When a new collaborator joins:
380
+ # Create server instance
381
+ server = LoroWebSocketServer(
382
+ port=8081, # Server port
383
+ host="localhost" # Server host
384
+ )
271
385
 
272
- 1. **Connection**: New client connects to WebSocket server
273
- 2. **Welcome**: Server sends welcome message to new client
274
- 3. **Snapshot Request**: New client requests current document state
275
- 4. **Snapshot Delivery**: Server sends stored snapshot or requests one from existing clients
276
- 5. **Content Sync**: New client applies snapshot and sees current document content
277
- 6. **Ready to Collaborate**: New client can now participate in real-time editing
386
+ # Start server
387
+ await server.start()
278
388
 
279
- The server maintains the latest document snapshot to ensure new collaborators always see existing content.
389
+ # Shutdown server
390
+ await server.shutdown()
391
+ ```
280
392
 
281
- ## Configuration
393
+ ### Supported Message Types
282
394
 
283
- ### WebSocket Server URL
395
+ The server handles these WebSocket message types:
284
396
 
285
- You can configure the WebSocket server URL in the UI or by modifying the default in `CollaborativeEditor.tsx`:
397
+ - `loro-update`: Apply CRDT document updates
398
+ - `snapshot`: Full document state snapshots
399
+ - `request-snapshot`: Request current document state
400
+ - `ephemeral-update`: Cursor and selection updates
401
+ - `awareness-update`: User presence information
286
402
 
287
- ```typescript
288
- const [websocketUrl, setWebsocketUrl] = useState('ws://localhost:8080')
289
- ```
403
+ ## Examples
290
404
 
291
- ### Server Port
405
+ For complete working examples and demonstrations, see the `src/examples/` directory:
292
406
 
293
- To change the server port, modify `server.ts`:
407
+ ```bash
408
+ # Run the example application
409
+ npm install
410
+ npm run example
294
411
 
295
- ```typescript
296
- const server = new LoroWebSocketServer(8080); // Change port here
412
+ # This starts both Node.js and Python servers plus a React demo app
413
+ # Open http://localhost:5173 to see dual editor interface
297
414
  ```
298
415
 
299
- ## Development Scripts
300
-
301
- - `npm run dev` - Start React development server
302
- - `npm run server` - Start WebSocket server
303
- - `npm run dev:all` - Start both server and client
304
- - `npm run build` - Build for production
305
- - `npm run lint` - Run ESLint
306
- - `npm run preview` - Preview production build
416
+ The examples include:
417
+ - **Complete React App**: Full collaborative editor with UI
418
+ - **Server Selection**: Switch between Node.js and Python backends
419
+ - **Dual Editors**: Simple text area and rich Lexical editor
420
+ - **Real-time Demo**: Multi-user collaboration testing
307
421
 
308
- ## Production Deployment
422
+ See `src/examples/README.md` for detailed example documentation.
309
423
 
310
- 1. Build the application:
311
- ```bash
312
- npm run build
313
- ```
314
-
315
- 2. Deploy the `dist` folder to your web server
424
+ ## Project Structure
316
425
 
317
- 3. Deploy the WebSocket server to your backend infrastructure
426
+ ### Core Components
318
427
 
319
- 4. Update the WebSocket URL in the application to point to your production server
428
+ ```
429
+ src/
430
+ ├── LoroCollaborativePlugin.tsx # Main Lexical plugin for collaboration
431
+ └── vite-env.d.ts # TypeScript definitions
320
432
 
321
- ## Contributing
433
+ lexical_loro/ # Python WebSocket server package
434
+ ├── __init__.py # Package exports
435
+ ├── server.py # WebSocket server implementation
436
+ ├── cli.py # Command line interface
437
+ └── tests/ # Python test suite
322
438
 
323
- 1. Fork the repository
324
- 2. Create a feature branch
325
- 3. Make your changes
326
- 4. Add tests if applicable
327
- 5. Submit a pull request
439
+ pyproject.toml # Python package configuration
440
+ ```
328
441
 
329
- ## License
442
+ ### Examples Directory
330
443
 
331
- This project is open source and available under the [MIT License](LICENSE).
444
+ ```
445
+ src/examples/ # Complete demo application
446
+ ├── App.tsx # Demo app with dual editors
447
+ ├── LexicalCollaborativeEditor.tsx # Rich text editor example
448
+ ├── TextAreaCollaborativeEditor.tsx # Simple text editor example
449
+ ├── ServerSelector.tsx # Server selection UI
450
+ ├── LexicalToolbar.tsx # Rich text toolbar
451
+ ├── main.tsx # Demo app entry point
452
+ └── *.css # Styling for examples
453
+
454
+ servers/
455
+ └── server.ts # Node.js server (for comparison)
456
+ ```
332
457
 
333
- ## Acknowledgments
458
+ ### Archive
334
459
 
335
- - [Loro CRDT](https://loro.dev/) - The CRDT library powering collaborative editing
336
- - [Vite](https://vitejs.dev/) - Fast development build tool
337
- - [React](https://reactjs.org/) - UI library
338
- - [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API)
460
+ ```
461
+ src/archive/ # Historical plugin implementations
462
+ ├── LoroCollaborativePlugin0.tsx # Previous versions for reference
463
+ ├── LoroCollaborativePlugin1.tsx
464
+ ├── LoroCollaborativePlugin2.tsx
465
+ ├── LoroCollaborativePlugin3.tsx
466
+ ├── LoroCollaborativePlugin4.tsx
467
+ └── LoroCollaborativePlugin5.tsx
468
+ ```
339
469
 
340
- # Lexical Loro Python Package
470
+ ## How It Works
341
471
 
342
- A Python package for Lexical + Loro CRDT integration, providing a WebSocket server for real-time collaborative text editing.
472
+ ### Architecture Overview
343
473
 
344
- ## Features
474
+ The collaboration system consists of two main components:
345
475
 
346
- - **Real-time collaboration**: WebSocket server for live document collaboration
347
- - **Loro CRDT integration**: Uses Loro CRDT for conflict-free replicated data types
348
- - **Lexical compatibility**: Designed to work with Lexical rich text editor
349
- - **Ephemeral data support**: Handles cursor positions and selections
350
- - **Multiple document support**: Manages multiple collaborative documents
476
+ 1. **LoroCollaborativePlugin** (Client-side)
477
+ - Integrates with Lexical editor as a React plugin
478
+ - Captures text changes and applies them to Loro CRDT document
479
+ - Sends/receives updates via WebSocket connection
480
+ - Handles cursor positioning and user awareness
351
481
 
352
- ## Installation
482
+ 2. **LoroWebSocketServer** (Server-side)
483
+ - Python WebSocket server using loro-py
484
+ - Maintains authoritative document state
485
+ - Broadcasts updates to all connected clients
486
+ - Handles client connections and disconnections
353
487
 
354
- ### From PyPI (when published)
488
+ ### Data Flow
355
489
 
356
- ```bash
357
- pip install lexical-loro
358
490
  ```
359
-
360
- ### Local Development
361
-
362
- ```bash
363
- # Install in development mode
364
- pip install -e "python_src/[dev]"
491
+ User Types → Lexical Editor → Plugin → Loro CRDT → WebSocket
492
+
493
+ WebSocket ← Loro CRDT ← Plugin ← Lexical Editor ← Other Users
365
494
  ```
366
495
 
367
- ## Usage
496
+ ### CRDT Integration Process
368
497
 
369
- ### Command Line
498
+ 1. **Document Creation**: Plugin creates a Loro document with unique identifier
499
+ 2. **Local Changes**: User edits trigger Lexical change events
500
+ 3. **CRDT Application**: Changes are applied to local Loro document
501
+ 4. **Synchronization**: Updates are serialized and sent via WebSocket
502
+ 5. **Remote Application**: Other clients receive and apply updates
503
+ 6. **Conflict Resolution**: Loro CRDT automatically merges changes without conflicts
370
504
 
371
- Start the server using the command line interface:
505
+ ### Connection Management
372
506
 
373
- ```bash
374
- # Start server on default port (8081)
375
- lexical-loro-server
507
+ - **Auto-reconnection**: Plugin handles connection drops gracefully
508
+ - **State Synchronization**: New clients receive full document snapshot
509
+ - **Error Handling**: Connection errors are logged and displayed
510
+ - **User Awareness**: Track online users and cursor positions
376
511
 
377
- # Start server on custom port
378
- lexical-loro-server --port 8082
512
+ ### Lexical Integration
379
513
 
380
- # Start with debug logging
381
- lexical-loro-server --log-level DEBUG
382
- ```
514
+ The Lexical editor integration includes:
383
515
 
384
- ### Programmatic Usage
516
+ 1. **LoroCollaborativePlugin**: A custom Lexical plugin that bridges Lexical and Loro CRDT
517
+ 2. **Bidirectional Sync**: Changes flow from Lexical → Loro → WebSocket and vice versa
518
+ 3. **Rich Text Preservation**: The plugin maintains rich text formatting during collaborative editing
519
+ 4. **Independent State**: Lexical editor maintains separate document state from simple text editor
385
520
 
386
- ```python
387
- import asyncio
388
- from lexical_loro import LoroWebSocketServer
521
+ ### WebSocket Communication
389
522
 
390
- async def main():
391
- server = LoroWebSocketServer(port=8081)
392
- await server.start()
523
+ The WebSocket server:
524
+ - Maintains connections to all clients
525
+ - Broadcasts Loro document updates to all connected clients with document ID filtering
526
+ - Handles client connections and disconnections
527
+ - Provides connection status feedback
528
+ - Stores separate snapshots for each document type
393
529
 
394
- if __name__ == "__main__":
395
- asyncio.run(main())
396
- ```
530
+ ### Real-time Updates
397
531
 
398
- ### Integration with Node.js/TypeScript Projects
532
+ 1. User types in the text area
533
+ 2. Change is applied to local Loro document
534
+ 3. Document update is serialized and sent via WebSocket
535
+ 4. Other clients receive the update and apply it to their documents
536
+ 5. UI is updated to reflect the changes
399
537
 
400
- Update your `package.json` scripts:
538
+ ### Initial Content Synchronization
401
539
 
402
- ```json
403
- {
404
- "scripts": {
405
- "server:py": "lexical-loro-server",
406
- "dev:py": "concurrently \"lexical-loro-server\" \"npm run dev\""
407
- }
408
- }
409
- ```
540
+ When a new collaborator joins:
410
541
 
411
- ## API Reference
542
+ 1. **Connection**: New client connects to WebSocket server
543
+ 2. **Welcome**: Server sends welcome message to new client
544
+ 3. **Snapshot Request**: New client requests current document state
545
+ 4. **Snapshot Delivery**: Server sends stored snapshot or requests one from existing clients
546
+ 5. **Content Sync**: New client applies snapshot and sees current document content
547
+ 6. **Ready to Collaborate**: New client can now participate in real-time editing
412
548
 
413
- ### LoroWebSocketServer
549
+ The server maintains the latest document snapshot to ensure new collaborators always see existing content.
414
550
 
415
- Main server class for handling WebSocket connections and Loro document management.
551
+ ## Configuration
416
552
 
417
- #### Constructor
553
+ ### Plugin Configuration
418
554
 
419
- ```python
420
- server = LoroWebSocketServer(port=8081)
555
+ ```tsx
556
+ <LoroCollaborativePlugin
557
+ websocketUrl="ws://localhost:8081" // Server URL
558
+ docId="my-document" // Document identifier
559
+ username="user123" // User identifier
560
+ userColor="#ff0000" // Cursor color (optional)
561
+ debug={true} // Enable debug logs (optional)
562
+ />
421
563
  ```
422
564
 
423
- #### Methods
565
+ ### Server Configuration
424
566
 
425
- - `start()`: Start the WebSocket server
426
- - `shutdown()`: Gracefully shutdown the server
427
- - `handle_client(websocket)`: Handle new client connections
428
- - `handle_message(client_id, message)`: Process messages from clients
429
-
430
- ### Client
567
+ ```python
568
+ # Via command line
569
+ lexical-loro-server --port 8081 --host localhost --log-level DEBUG
431
570
 
432
- Represents a connected client with metadata.
571
+ # Via environment variables
572
+ export LEXICAL_LORO_PORT=8081
573
+ export LEXICAL_LORO_HOST=localhost
574
+ export LEXICAL_LORO_LOG_LEVEL=DEBUG
575
+ lexical-loro-server
433
576
 
434
- ```python
435
- class Client:
436
- def __init__(self, websocket, client_id):
437
- self.websocket = websocket
438
- self.id = client_id
439
- self.color = self._generate_color()
577
+ # Programmatically
578
+ server = LoroWebSocketServer(port=8081, host="localhost")
440
579
  ```
441
580
 
581
+ ### Supported Document Types
582
+
583
+ The server supports multiple document types with different IDs:
584
+ - `shared-text`: Basic text collaboration
585
+ - `lexical-shared-doc`: Rich text with Lexical
586
+ - Custom document IDs for multiple simultaneous documents
587
+
442
588
  ## Development
443
589
 
444
- ### Setup Development Environment
590
+ ### Core Components Development
445
591
 
592
+ **Plugin Development:**
446
593
  ```bash
447
- # Install development dependencies
448
- pip install -e "python_src/[dev]"
594
+ # The plugin is a single TypeScript file
595
+ src/LoroCollaborativePlugin.tsx
449
596
 
450
- # Run tests
451
- pytest
452
-
453
- # Run tests with coverage
454
- pytest --cov=lexical_loro --cov-report=html
597
+ # Dependencies for plugin development
598
+ npm install lexical @lexical/react @lexical/selection loro-crdt
599
+ ```
455
600
 
456
- # Format code
457
- black python_src/
601
+ **Server Development:**
602
+ ```bash
603
+ # Install Python package in development mode
604
+ pip install -e ".[dev]"
458
605
 
459
- # Lint code
460
- ruff python_src/
606
+ # Run tests
607
+ pytest lexical_loro/tests/ -v
461
608
 
462
- # Type checking
463
- mypy python_src/
609
+ # Start server in development mode
610
+ python3 -m lexical_loro.cli --port 8081 --log-level DEBUG
464
611
  ```
465
612
 
466
613
  ### Testing
467
614
 
468
- The package includes comprehensive tests for:
469
-
470
- - WebSocket connection handling
471
- - Loro document operations
472
- - Message processing
473
- - Client management
474
- - Error handling
475
-
476
- Run tests:
477
-
615
+ **Plugin Testing:**
478
616
  ```bash
479
- pytest tests/ -v
617
+ npm run test # Run Vitest tests
618
+ npm run test:js # Run tests once
480
619
  ```
481
620
 
482
- ### Building
483
-
484
- Build the package:
485
-
621
+ **Server Testing:**
486
622
  ```bash
487
- pip install build
488
- python -m build
623
+ npm run test:py # Run Python tests
624
+ npm run test:py:watch # Run in watch mode
625
+ npm run test:py:coverage # Run with coverage
489
626
  ```
490
627
 
491
- ## Protocol
492
-
493
- The server communicates with clients using a JSON-based WebSocket protocol:
494
-
495
- ### Message Types
496
-
497
- - `loro-update`: Apply Loro CRDT updates
498
- - `snapshot`: Full document snapshots
499
- - `request-snapshot`: Request current document state
500
- - `ephemeral-update`: Cursor and selection updates
501
- - `awareness-update`: User presence information
628
+ ### Example Development
502
629
 
503
- ### Example Messages
504
-
505
- ```json
506
- {
507
- "type": "loro-update",
508
- "docId": "lexical-shared-doc",
509
- "updateHex": "deadbeef..."
510
- }
630
+ To work on the examples:
631
+ ```bash
632
+ npm install # Install all dependencies
633
+ npm run example # Start example app with both servers
634
+ npm run example:py # Start with Python server only
635
+ npm run example:js # Start with Node.js server only
636
+ npm run example:vite # Start example app only (no servers)
511
637
  ```
512
638
 
513
- ## Configuration
514
-
515
- ### Environment Variables
639
+ ## Contributing
516
640
 
517
- - `LEXICAL_LORO_PORT`: Default server port (default: 8081)
518
- - `LEXICAL_LORO_HOST`: Host to bind to (default: localhost)
519
- - `LEXICAL_LORO_LOG_LEVEL`: Logging level (default: INFO)
641
+ We welcome contributions to both the Lexical plugin and Python server:
520
642
 
521
- ### Supported Documents
643
+ 1. Fork the repository
644
+ 2. Create a feature branch
645
+ 3. Focus changes on core components:
646
+ - `src/LoroCollaborativePlugin.tsx` for plugin improvements
647
+ - `lexical_loro/` for server enhancements
648
+ 4. Add tests for new functionality
649
+ 5. Update documentation as needed
650
+ 6. Submit a pull request
522
651
 
523
- The server pre-initializes several document types:
652
+ ### Development Guidelines
524
653
 
525
- - `shared-text`: Basic text document
526
- - `lexical-shared-doc-v0`: Minimal plugin document
527
- - `lexical-shared-doc-v1`: Full-featured plugin document
528
- - `lexical-shared-doc-v2`: Clean JSON plugin document
529
- - `lexical-shared-doc-v3`: Text-only plugin document
530
- - `lexical-shared-doc-v4`: Smart hybrid plugin document
654
+ - **Plugin**: Keep the plugin self-contained and dependency-light
655
+ - **Server**: Maintain compatibility with loro-py and WebSocket standards
656
+ - **Examples**: Use examples to demonstrate new features
657
+ - **Tests**: Ensure both JavaScript and Python tests pass
531
658
 
532
659
  ## License
533
660
 
534
- MIT License - see LICENSE file for details.
535
-
536
- ## Contributing
537
-
538
- 1. Fork the repository
539
- 2. Create a feature branch
540
- 3. Make your changes
541
- 4. Add tests
542
- 5. Run the test suite
543
- 6. Submit a pull request
544
-
545
- ## Support
546
-
547
- For issues and questions:
661
+ This project is open source and available under the [MIT License](LICENSE).
548
662
 
549
- - GitHub Issues: https://github.com/datalayer/lexical-loro/issues
550
- - Documentation: https://github.com/datalayer/lexical-loro#readme
663
+ ## Acknowledgments
551
664
 
665
+ - [Loro CRDT](https://loro.dev/) - The CRDT library powering collaborative editing
666
+ - [Lexical](https://lexical.dev/) - Facebook's extensible text editor framework
667
+ - [React](https://reactjs.org/) - UI library for plugin hooks
668
+ - [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) - Real-time communication
@@ -9,6 +9,7 @@ interface LoroCollaborativePluginProps {
9
9
  userName: string;
10
10
  isCurrentUser?: boolean;
11
11
  }>) => void;
12
+ onInitialization?: (success: boolean) => void;
12
13
  }
13
- export declare function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChange, onDisconnectReady, onPeerIdChange, onAwarenessChange }: LoroCollaborativePluginProps): import("react/jsx-runtime").JSX.Element;
14
+ export declare function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChange, onDisconnectReady, onPeerIdChange, onAwarenessChange, onInitialization }: LoroCollaborativePluginProps): import("react/jsx-runtime").JSX.Element;
14
15
  export default LoroCollaborativePlugin;
@@ -1,4 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /*
3
+ * Copyright (c) 2023-2025 Datalayer, Inc.
4
+ * Distributed under the terms of the MIT License.
5
+ */
2
6
  import { useEffect, useRef, useCallback, useState } from 'react';
3
7
  import { createPortal } from 'react-dom';
4
8
  import { $createParagraphNode, $getRoot, $getSelection, $isRangeSelection, $getNodeByKey, $isTextNode, $isElementNode, $isLineBreakNode, $createTextNode, createState, $getState, $setState } from 'lexical';
@@ -623,7 +627,7 @@ class CursorAwareness {
623
627
  return this.ephemeralStore.getAllStates();
624
628
  }
625
629
  }
626
- export function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChange, onDisconnectReady, onPeerIdChange, onAwarenessChange }) {
630
+ export function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChange, onDisconnectReady, onPeerIdChange, onAwarenessChange, onInitialization }) {
627
631
  const [editor] = useLexicalComposerContext();
628
632
  const wsRef = useRef(null);
629
633
  const loroDocRef = useRef(new LoroDoc());
@@ -1686,6 +1690,10 @@ export function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChang
1686
1690
  loroDocRef.current.import(snapshot);
1687
1691
  hasReceivedInitialSnapshot.current = true;
1688
1692
  console.log('📄 Lexical editor received and applied initial snapshot');
1693
+ // Notify parent component about successful initialization
1694
+ if (onInitialization) {
1695
+ onInitialization(true);
1696
+ }
1689
1697
  // Immediately reflect the current Loro text into the editor after import
1690
1698
  try {
1691
1699
  const currentText = loroDocRef.current.getText(docId).toString();
@@ -1693,6 +1701,10 @@ export function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChang
1693
1701
  }
1694
1702
  catch (e) {
1695
1703
  console.warn('⚠️ Could not immediately reflect snapshot to editor:', e);
1704
+ // Notify parent component about failed initialization
1705
+ if (onInitialization) {
1706
+ onInitialization(false);
1707
+ }
1696
1708
  }
1697
1709
  }
1698
1710
  else if (data.type === 'ephemeral-update' || data.type === 'ephemeral-event') {
@@ -1842,6 +1854,10 @@ export function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChang
1842
1854
  ws.onerror = (err) => {
1843
1855
  isConnectingRef.current = false;
1844
1856
  console.error('WebSocket error in Lexical plugin:', err);
1857
+ // Notify initialization failure if we haven't received initial content yet
1858
+ if (!hasReceivedInitialSnapshot.current && onInitialization) {
1859
+ onInitialization(false);
1860
+ }
1845
1861
  };
1846
1862
  }
1847
1863
  catch (err) {
package/lib/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./LoroCollaborativePlugin";
package/lib/index.js ADDED
@@ -0,0 +1,5 @@
1
+ /*
2
+ * Copyright (c) 2023-2025 Datalayer, Inc.
3
+ * Distributed under the terms of the MIT License.
4
+ */
5
+ export * from "./LoroCollaborativePlugin";
package/package.json CHANGED
@@ -1,10 +1,22 @@
1
1
  {
2
2
  "name": "@datalayer/lexical-loro",
3
3
  "private": false,
4
- "version": "0.0.1",
4
+ "version": "0.0.2",
5
5
  "type": "module",
6
+ "description": "Collaborative editing plugin for Lexical based on Loro CRDT",
7
+ "main": "lib/index.js",
8
+ "types": "lib/index.d.ts",
6
9
  "files": [
7
- "lib/**/*"
10
+ "lib/**/*.*"
11
+ ],
12
+ "keywords": [
13
+ "lexical",
14
+ "loro",
15
+ "crdt",
16
+ "collaborative",
17
+ "editor",
18
+ "plugin",
19
+ "react"
8
20
  ],
9
21
  "repository": {
10
22
  "type": "git",
@@ -18,28 +30,34 @@
18
30
  "scripts": {
19
31
  "build": "tsc -b && vite build",
20
32
  "clean": "rimraf dist lib",
33
+ "dev": "npm run example",
34
+ "dev:all:js": "npm run example:js",
35
+ "dev:all:py": "npm run example:py",
36
+ "dev:vite": "npm run example:vite",
37
+ "example": "concurrently \"npm run server\" \"npm run server:py\" \"npm run example:vite\"",
38
+ "example:js": "concurrently \"npm run server\" \"npm run example:vite\"",
39
+ "example:py": "concurrently \"npm run server:py\" \"npm run example:vite\"",
40
+ "example:vite": "vite",
21
41
  "lint": "eslint .",
22
42
  "preview": "vite preview",
23
43
  "server": "tsx servers/server.ts",
24
44
  "server:py": "lexical-loro-server",
25
45
  "server:py:dev": "python3 -m lexical_loro.cli",
26
- "test:py": "python3 -m pytest lexical_loro/tests/ -v",
27
- "test:py:watch": "python3 -m pytest lexical_loro/tests/ -v --tb=short -f",
28
- "test:py:coverage": "python3 -m pytest lexical_loro/tests/ --cov=. --cov-report=html --cov-report=term",
29
46
  "test": "vitest",
30
47
  "test:js": "vitest run",
31
- "dev": "concurrently \"npm run server\" \"npm run server:py\" \"npm run dev:vite\"",
32
- "dev:vite": "vite",
33
- "dev:all:py": "concurrently \"npm run server:py\" \"npm run dev:vite\"",
34
- "dev:all:js": "concurrently \"npm run server\" \"npm run dev:vite\""
48
+ "test:py": "python3 -m pytest lexical_loro/tests/ -v",
49
+ "test:py:coverage": "python3 -m pytest lexical_loro/tests/ --cov=. --cov-report=html --cov-report=term",
50
+ "test:py:watch": "python3 -m pytest lexical_loro/tests/ -v --tb=short -f"
35
51
  },
36
52
  "dependencies": {
37
- "@lexical/react": "^0.33.1",
38
- "@lexical/selection": "^0.33.1",
39
- "lexical": "^0.33.1",
40
53
  "loro-crdt": "^1.5.10",
41
54
  "react": "^18 || ^19.1.0",
42
- "react-dom": "^18 || ^19.1.0"
55
+ "react-dom": "^18 || ^19.1.0",
56
+ "lexical": "^0.33.1",
57
+ "@lexical/react": "^0.33.1",
58
+ "@lexical/selection": "^0.33.1"
59
+ },
60
+ "peerDependencies": {
43
61
  },
44
62
  "devDependencies": {
45
63
  "@eslint/js": "^9.30.1",