@dxos/react-ui-editor 0.3.11-main.00a28cb → 0.3.11-main.08776d7

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 (45) hide show
  1. package/dist/lib/browser/index.mjs +258 -252
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/components/TextEditor/TextEditor.d.ts +4 -2
  5. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +1 -1
  7. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  8. package/dist/types/src/extensions/automerge/defs.d.ts +5 -7
  9. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
  10. package/dist/types/src/extensions/automerge/semaphore.d.ts +5 -4
  11. package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -1
  12. package/dist/types/src/extensions/comments.d.ts +12 -12
  13. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  14. package/dist/types/src/extensions/cursor.d.ts +3 -3
  15. package/dist/types/src/extensions/cursor.d.ts.map +1 -1
  16. package/dist/types/src/extensions/index.d.ts +1 -0
  17. package/dist/types/src/extensions/index.d.ts.map +1 -1
  18. package/dist/types/src/extensions/sync.d.ts +7 -0
  19. package/dist/types/src/extensions/sync.d.ts.map +1 -0
  20. package/dist/types/src/hooks/automerge.d.ts +2 -1
  21. package/dist/types/src/hooks/automerge.d.ts.map +1 -1
  22. package/dist/types/src/hooks/defs.d.ts +28 -0
  23. package/dist/types/src/hooks/defs.d.ts.map +1 -0
  24. package/dist/types/src/hooks/index.d.ts +1 -0
  25. package/dist/types/src/hooks/index.d.ts.map +1 -1
  26. package/dist/types/src/hooks/useTextModel.d.ts +2 -27
  27. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  28. package/dist/types/src/hooks/yjs.d.ts +2 -1
  29. package/dist/types/src/hooks/yjs.d.ts.map +1 -1
  30. package/package.json +23 -23
  31. package/src/components/TextEditor/MarkdownEditor.stories.tsx +5 -5
  32. package/src/components/TextEditor/TextEditor.tsx +46 -38
  33. package/src/components/TextEditor/comments.stories.tsx +35 -28
  34. package/src/extensions/automerge/automerge.ts +10 -39
  35. package/src/extensions/automerge/defs.ts +8 -13
  36. package/src/extensions/automerge/semaphore.ts +14 -7
  37. package/src/extensions/comments.ts +137 -126
  38. package/src/extensions/cursor.ts +7 -3
  39. package/src/extensions/index.ts +1 -0
  40. package/src/extensions/sync.ts +15 -0
  41. package/src/hooks/automerge.ts +2 -1
  42. package/src/hooks/defs.ts +47 -0
  43. package/src/hooks/index.ts +1 -0
  44. package/src/hooks/useTextModel.ts +2 -45
  45. package/src/hooks/yjs.ts +2 -1
@@ -10,9 +10,9 @@ import { debounce } from '@dxos/async';
10
10
  import { invariant } from '@dxos/invariant';
11
11
  import { nonNullable } from '@dxos/util';
12
12
 
13
- import { semaphoreFacet } from './automerge/defs';
14
13
  import { Cursor } from './cursor';
15
- import { type CommentRange, type EditorModel, type Range } from '../hooks';
14
+ import { syncFacet } from './sync';
15
+ import { type Comment, type Range } from '../hooks';
16
16
  import { getToken } from '../styles';
17
17
  import { callbackWrapper } from '../util';
18
18
 
@@ -31,39 +31,38 @@ const marks = {
31
31
  highlightActive: Decoration.mark({ class: 'cm-comment-active' }),
32
32
  };
33
33
 
34
- // TODO(burdon): Should this be part of state?
35
- type CommentSelected = {
34
+ type CommentState = {
35
+ comment: Comment;
36
+ range: Range;
37
+ location?: Rect | null;
38
+ };
39
+
40
+ type SelectionState = {
36
41
  active?: string;
37
42
  closest?: string;
38
43
  };
39
44
 
40
- // TODO(burdon): Rename.
41
- type ExtendedCommentRange = Range &
42
- CommentRange & {
43
- // TODO(burdon): Not part of state; just required for callback.
44
- location?: Rect | null;
45
- };
46
-
47
- export type CommentsState = CommentSelected & {
48
- ranges: ExtendedCommentRange[];
45
+ export type CommentsState = {
46
+ comments: CommentState[];
47
+ selection: SelectionState;
49
48
  };
50
49
 
51
50
  export const setFocus = (view: EditorView, thread: string) => {
52
- const range = view.state.field(commentsStateField).ranges.find((range) => range.id === thread);
53
- if (!range) {
51
+ const comment = view.state.field(commentsState).comments.find((range) => range.comment.id === thread);
52
+ if (!comment) {
54
53
  return;
55
54
  }
56
55
 
57
56
  view.dispatch({
58
57
  effects: setSelection.of({ active: thread }),
59
- selection: { anchor: range.from },
58
+ selection: { anchor: comment.range.from },
60
59
  scrollIntoView: true, // TODO(burdon): Scroll to y-position (or center of screen?)
61
60
  });
62
61
  };
63
62
 
64
- export const setCommentRange = StateEffect.define<{ model: EditorModel; comments: CommentRange[] }>();
63
+ export const setComments = StateEffect.define<Comment[]>();
65
64
 
66
- export const setSelection = StateEffect.define<CommentSelected>();
65
+ export const setSelection = StateEffect.define<SelectionState>();
67
66
 
68
67
  const setCommentState = StateEffect.define<CommentsState>();
69
68
 
@@ -71,29 +70,31 @@ const setCommentState = StateEffect.define<CommentsState>();
71
70
  * State field (reducer) that tracks comment ranges.
72
71
  * The ranges are tracked as Automerge cursors from which the absolute indexed ranges can be computed.
73
72
  */
74
- const commentsStateField = StateField.define<CommentsState>({
75
- create: () => ({ ranges: [] }),
73
+ const commentsState = StateField.define<CommentsState>({
74
+ create: () => ({ comments: [], selection: {} }),
76
75
  update: (value, tr) => {
77
- const cursorConverter = tr.state.facet(Cursor.converter);
78
-
79
76
  for (const effect of tr.effects) {
80
77
  // Update selection.
81
78
  if (effect.is(setSelection)) {
82
- return { ...value, ...effect.value };
79
+ return { ...value, selection: effect.value };
83
80
  }
84
81
 
85
82
  // Update range from store.
86
- if (effect.is(setCommentRange)) {
87
- const { comments } = effect.value;
88
- const ranges: ExtendedCommentRange[] = comments
83
+ if (effect.is(setComments)) {
84
+ const comments: CommentState[] = effect.value
89
85
  .map((comment) => {
90
- const range = Cursor.getRangeFromCursor(cursorConverter, comment.cursor);
86
+ // Skip cut/deleted comments.
87
+ if (!comment.cursor) {
88
+ return undefined;
89
+ }
90
+
91
+ const range = Cursor.getRangeFromCursor(tr.state, comment.cursor);
91
92
  // console.log('update', JSON.stringify({ range, cursor: comment.cursor }, undefined, 2));
92
- return range && { ...comment, ...range };
93
+ return range && { comment, range };
93
94
  })
94
95
  .filter(nonNullable);
95
96
 
96
- return { ...value, ranges };
97
+ return { ...value, comments };
97
98
  }
98
99
 
99
100
  // Update entire state.
@@ -109,24 +110,27 @@ const commentsStateField = StateField.define<CommentsState>({
109
110
  /**
110
111
  * Decorate ranges.
111
112
  */
112
- const highlightDecorations = EditorView.decorations.compute([commentsStateField], (state) => {
113
- const { active, ranges } = state.field(commentsStateField);
114
- const decorations =
115
- sortBy(ranges ?? [], (range) => range.from)
116
- ?.flatMap((selection) => {
117
- // TODO(burdon): Invalid range (e.g., deleted).
118
- if (selection.from === selection.to) {
119
- return undefined;
120
- }
113
+ const highlightDecorations = EditorView.decorations.compute([commentsState], (state) => {
114
+ const {
115
+ selection: { active },
116
+ comments,
117
+ } = state.field(commentsState);
118
+
119
+ const decorations = sortBy(comments ?? [], (range) => range.range.from)
120
+ ?.flatMap((comment) => {
121
+ const range = comment.range;
122
+ if (!range || range.from === range.to) {
123
+ console.warn('Invalid range:', range);
124
+ return undefined;
125
+ }
121
126
 
122
- const range = { from: selection.from, to: selection.to };
123
- if (selection.id === active) {
124
- return marks.highlightActive.range(range.from, range.to);
125
- } else {
126
- return marks.highlight.range(range.from, range.to);
127
- }
128
- })
129
- .filter(nonNullable) ?? [];
127
+ if (comment.comment.id === active) {
128
+ return marks.highlightActive.range(range.from, range.to);
129
+ } else {
130
+ return marks.highlight.range(range.from, range.to);
131
+ }
132
+ })
133
+ .filter(nonNullable);
130
134
 
131
135
  return Decoration.set(decorations);
132
136
  });
@@ -162,14 +166,15 @@ export type CommentsOptions = {
162
166
  const trackPastedComments = (onUpdate: NonNullable<CommentsOptions['onUpdate']>) => {
163
167
  // Cut/deleted comments.
164
168
  // Tracks indexed selections within text.
169
+ // TODO(burdon): Move to main state field?
165
170
  let tracked: { text: Text; comments: { id: string; from: number; to: number }[] } | null = null;
166
171
 
167
172
  // Track cut or copy (enables cut-and-paste and copy-delete-paste to restore comment selection).
168
173
  const handleTrack = (event: Event, view: EditorView) => {
169
- const comments = view.state.field(commentsStateField);
174
+ const comments = view.state.field(commentsState);
170
175
  const { main } = view.state.selection;
171
- const selectedRanges = comments.ranges.filter(
172
- (range) => range.from >= main.from && range.to <= main.to && range.from < range.to,
176
+ const selectedRanges = comments.comments.filter(
177
+ ({ range }) => range.from >= main.from && range.to <= main.to && range.from < range.to,
173
178
  );
174
179
 
175
180
  if (!selectedRanges.length) {
@@ -177,8 +182,8 @@ const trackPastedComments = (onUpdate: NonNullable<CommentsOptions['onUpdate']>)
177
182
  } else {
178
183
  tracked = {
179
184
  text: view.state.doc.slice(main.from, main.to),
180
- comments: selectedRanges.map((range) => ({
181
- id: range.id,
185
+ comments: selectedRanges.map(({ comment, range }) => ({
186
+ id: comment.id,
182
187
  from: range.from - main.from,
183
188
  to: range.to - main.from,
184
189
  })),
@@ -211,24 +216,19 @@ const trackPastedComments = (onUpdate: NonNullable<CommentsOptions['onUpdate']>)
211
216
  });
212
217
 
213
218
  if (found > -1) {
214
- const comments = tracked.comments;
215
-
216
219
  // Sync before recomputing cursor/range.
217
- // TODO(burdon): Generalize sync facet to decouple from automerge?
218
- view.state.facet(semaphoreFacet).reconcile(view);
219
-
220
- const active = state.field(commentsStateField).ranges;
221
- for (const moved of comments) {
222
- if (active.some((range) => range.id === moved.id && range.from === range.to)) {
223
- const range = {
224
- from: found + moved.from,
225
- to: found + moved.to,
226
- };
227
-
228
- const cursor = Cursor.getCursorFromRange(state.facet(Cursor.converter), range);
229
- // console.log('paste', JSON.stringify({ moved, range, cursor }, undefined, 2));
230
- onUpdate(moved.id, cursor);
231
- }
220
+ // TODO(burdon): Try to avoid this? See TextEditor dispatch.
221
+ state.facet(syncFacet)?.reconcile(view);
222
+
223
+ for (const moved of tracked.comments) {
224
+ const range = {
225
+ from: found + moved.from,
226
+ to: found + moved.to,
227
+ };
228
+
229
+ const cursor = Cursor.getCursorFromRange(state, range);
230
+ // console.log('paste', JSON.stringify({ moved, range, cursor }, undefined, 2));
231
+ onUpdate(moved.id, cursor);
232
232
  }
233
233
  }
234
234
 
@@ -259,15 +259,23 @@ export const comments = (options: CommentsOptions = {}): Extension => {
259
259
  * Create comment thread action.
260
260
  */
261
261
  const createCommentThread: Command = (view) => {
262
- const cursorConverter = view.state.facet(Cursor.converter);
263
-
264
262
  invariant(options.onCreate);
265
263
  const { from, to } = view.state.selection.main;
266
264
  if (from === to) {
267
265
  return false;
268
266
  }
269
267
 
270
- const cursor = Cursor.getCursorFromRange(cursorConverter, { from, to });
268
+ // Don't allow selection at end of document.
269
+ if (to === view.state.doc.length) {
270
+ view.dispatch({
271
+ changes: {
272
+ from: to,
273
+ insert: '\n',
274
+ },
275
+ });
276
+ }
277
+
278
+ const cursor = Cursor.getCursorFromRange(view.state, { from, to });
271
279
  if (cursor) {
272
280
  // Create thread via callback.
273
281
  const id = callbackWrapper(options.onCreate)(cursor, view.coordsAtPos(from));
@@ -286,7 +294,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
286
294
  };
287
295
 
288
296
  return [
289
- commentsStateField,
297
+ commentsState,
290
298
  highlightDecorations,
291
299
  styles,
292
300
 
@@ -297,6 +305,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
297
305
  ? keymap.of([
298
306
  {
299
307
  key: shortcut,
308
+ // TODO(burdon): Check if there's a better way?
300
309
  run: callbackWrapper(createCommentThread),
301
310
  },
302
311
  ])
@@ -334,72 +343,74 @@ export const comments = (options: CommentsOptions = {}): Extension => {
334
343
  : [],
335
344
 
336
345
  //
337
- // Monitor cursor movement and text updates.
338
- // TODO(burdon): Is there a better (finer grained) way to do this?
346
+ // Track deleted ranges and update ranges for decorations.
339
347
  //
340
348
  EditorView.updateListener.of(({ view, state, changes }) => {
341
- const cursorConverter = view.state.facet(Cursor.converter);
342
-
343
- //
344
- // Test if need to recompute indexed range if document changes before the end of the range.
345
- //
346
- {
347
- let mod = false;
348
- const { active, ranges } = state.field(commentsStateField);
349
- changes.iterChanges((from, to, from2, to2) => {
350
- // TODO(burdon): Skip deleted (tracked) ranges.
351
- ranges.forEach((range) => {
352
- if (from2 === to2) {
353
- const newRange = Cursor.getRangeFromCursor(cursorConverter, range.cursor);
354
- if (!newRange || newRange.to - newRange.from === 0) {
355
- options.onDelete?.(range.id);
356
- }
349
+ let mod = false;
350
+ const { comments, ...value } = state.field(commentsState);
351
+ changes.iterChanges((from, to, from2, to2) => {
352
+ comments.forEach(({ comment, range }) => {
353
+ // Test if range deleted.
354
+ if (from2 === to2) {
355
+ const newRange = Cursor.getRangeFromCursor(view.state, comment.cursor!);
356
+ if (!newRange || newRange.to - newRange.from === 0) {
357
+ options.onDelete?.(comment.id);
357
358
  }
359
+ }
358
360
 
359
- if (from <= range.to) {
360
- const newRange = Cursor.getRangeFromCursor(cursorConverter, range.cursor);
361
- Object.assign(range, newRange);
362
- mod = true;
363
- }
364
- });
361
+ // Update range.
362
+ if (from <= range.to) {
363
+ const newRange = Cursor.getRangeFromCursor(view.state, comment.cursor!);
364
+ Object.assign(range, newRange);
365
+ mod = true;
366
+ }
365
367
  });
368
+ });
366
369
 
367
- if (mod) {
368
- view.dispatch({ effects: setCommentState.of({ active, ranges }) });
369
- }
370
+ if (mod) {
371
+ view.dispatch({ effects: setCommentState.of({ comments, ...value }) });
370
372
  }
373
+ }),
371
374
 
372
- //
373
- // Track the current selection.
374
- //
375
- {
376
- const { head } = state.selection.main;
377
-
378
- let min = Infinity;
379
- const { active, closest, ranges } = state.field(commentsStateField);
380
- const selected: CommentSelected = { active: undefined, closest: undefined };
381
-
382
- // TODO(burdon): Skip deleted (tracked) ranges.
383
- ranges.forEach((comment) => {
384
- const d = Math.min(Math.abs(head - comment.from), Math.abs(head - comment.to));
385
- if (head >= comment.from && head <= comment.to) {
386
- selected.active = comment.id;
387
- }
375
+ //
376
+ // Track selection/proximity.
377
+ //
378
+ EditorView.updateListener.of(({ view, state, changes }) => {
379
+ let min = Infinity;
380
+ const {
381
+ selection: { active, closest },
382
+ comments,
383
+ } = state.field(commentsState);
384
+
385
+ const { head } = state.selection.main;
386
+ const selection: SelectionState = {};
387
+ comments.forEach(({ comment, range }) => {
388
+ if (head >= range.from && head <= range.to) {
389
+ selection.active = comment.id;
390
+ selection.closest = undefined;
391
+ }
392
+
393
+ if (!selection.active) {
394
+ const d = Math.min(Math.abs(head - range.from), Math.abs(head - range.to));
388
395
  if (d < min) {
389
- selected.closest = comment.id;
396
+ selection.closest = comment.id;
390
397
  min = d;
391
398
  }
392
- });
393
-
394
- if (selected.active !== active || selected.closest !== closest) {
395
- view.dispatch({ effects: setSelection.of(selected) });
396
-
397
- // Update callback.
398
- handleSelect({
399
- ...selected,
400
- ranges: ranges.map((range) => ({ ...range, location: view.coordsAtPos(range.from) })),
401
- });
402
399
  }
400
+ });
401
+
402
+ if (selection.active !== active || selection.closest !== closest) {
403
+ view.dispatch({ effects: setSelection.of(selection) });
404
+
405
+ // Update callback.
406
+ handleSelect({
407
+ selection,
408
+ comments: comments.map(({ comment, range }) => ({
409
+ comment,
410
+ range,
411
+ location: view.coordsAtPos(range.from),
412
+ })),
413
+ });
403
414
  }
404
415
  }),
405
416
 
@@ -2,7 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { Facet } from '@codemirror/state';
5
+ import { type EditorState, Facet } from '@codemirror/state';
6
6
 
7
7
  import type { Range } from '../hooks';
8
8
 
@@ -31,13 +31,17 @@ export class Cursor {
31
31
  combine: (providers) => providers[0] ?? defaultCursorConverter,
32
32
  });
33
33
 
34
- static readonly getCursorFromRange = (cursorConverter: CursorConverter, range: Range) => {
34
+ static readonly getCursorFromRange = (state: EditorState, range: Range) => {
35
+ const cursorConverter = state.facet(Cursor.converter);
36
+
35
37
  const from = cursorConverter.toCursor(range.from);
36
38
  const to = cursorConverter.toCursor(range.to, -1);
37
39
  return [from, to].join(':');
38
40
  };
39
41
 
40
- static readonly getRangeFromCursor = (cursorConverter: CursorConverter, cursor: string) => {
42
+ static readonly getRangeFromCursor = (state: EditorState, cursor: string) => {
43
+ const cursorConverter = state.facet(Cursor.converter);
44
+
41
45
  const parts = cursor.split(':');
42
46
  const from = cursorConverter.fromCursor(parts[0]);
43
47
  const to = cursorConverter.fromCursor(parts[1]);
@@ -17,6 +17,7 @@ export * from './listener';
17
17
  export * from './markdown';
18
18
  export * from './mention';
19
19
  export * from './outliner';
20
+ export * from './sync';
20
21
  export * from './table';
21
22
  export * from './tasklist';
22
23
  export * from './typewriter';
@@ -0,0 +1,15 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { Facet } from '@codemirror/state';
6
+ import type { EditorView } from '@codemirror/view';
7
+
8
+ export interface Syncer {
9
+ // TODO(burdon): Rename?
10
+ reconcile(view: EditorView): void;
11
+ }
12
+
13
+ export const syncFacet = Facet.define<Syncer, Syncer>({
14
+ combine: (values) => values.at(-1)!, // Take last.
15
+ });
@@ -8,7 +8,8 @@ import { type AutomergeTextCompat, getRawDoc } from '@dxos/echo-schema';
8
8
  import { isNotNullOrUndefined } from '@dxos/util';
9
9
 
10
10
  import { SpaceAwarenessProvider } from './awareness-provider';
11
- import { type EditorModel, modelState, type UseTextModelProps } from './useTextModel';
11
+ import { type EditorModel, modelState } from './defs';
12
+ import { type UseTextModelProps } from './useTextModel';
12
13
  import { automerge, awareness, AwarenessProvider } from '../extensions';
13
14
  import { cursorColor } from '../styles';
14
15
 
@@ -0,0 +1,47 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type Extension, StateField } from '@codemirror/state';
6
+ import type * as YP from 'y-protocols/awareness';
7
+
8
+ import { type DocAccessor } from '@dxos/echo-schema';
9
+ import type { YText, YXmlFragment } from '@dxos/text-model';
10
+
11
+ // Runtime data structure.
12
+ export type Range = {
13
+ from: number;
14
+ to: number;
15
+ };
16
+
17
+ // Persistent data structure.
18
+ export type Comment = {
19
+ id: string;
20
+ cursor?: string;
21
+ };
22
+
23
+ export type EditorModel = {
24
+ id: string;
25
+ text: () => string;
26
+ extension?: Extension;
27
+
28
+ // TODO(burdon): Remove.
29
+ content: string | YText | YXmlFragment | DocAccessor;
30
+
31
+ // TODO(burdon): Move into extension.
32
+ awareness?: YP.Awareness;
33
+
34
+ // TODO(burdon): Remove.
35
+ peer?: {
36
+ id: string;
37
+ name?: string;
38
+ };
39
+ };
40
+
41
+ /**
42
+ * State field makes the model available to other extensions.
43
+ */
44
+ export const modelState = StateField.define<EditorModel | undefined>({
45
+ create: () => undefined,
46
+ update: (model) => model,
47
+ });
@@ -4,6 +4,7 @@
4
4
 
5
5
  export * from './automerge';
6
6
  export * from './awareness-provider';
7
+ export * from './defs';
7
8
  export * from './yjs';
8
9
 
9
10
  export * from './useTextModel';
@@ -2,56 +2,15 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { StateField, type Extension } from '@codemirror/state';
6
5
  import { useEffect, useState } from 'react';
7
- import type * as YP from 'y-protocols/awareness';
8
6
 
9
- import { isAutomergeObject, type DocAccessor, type Space, type TextObject } from '@dxos/react-client/echo';
7
+ import { isAutomergeObject, type Space, type TextObject } from '@dxos/react-client/echo';
10
8
  import { type Identity } from '@dxos/react-client/halo';
11
- import type { YText, YXmlFragment } from '@dxos/text-model';
12
9
 
13
10
  import { createAutomergeModel } from './automerge';
11
+ import { type EditorModel } from './defs';
14
12
  import { createYjsModel } from './yjs';
15
13
 
16
- // TODO(burdon): Factor out defs.
17
-
18
- /**
19
- * State field makes the model available to other extensions.
20
- */
21
- export const modelState = StateField.define<EditorModel | undefined>({
22
- create: () => undefined,
23
- update: (model) => model,
24
- });
25
-
26
- export type Range = {
27
- from: number;
28
- to: number;
29
- };
30
-
31
- export type CommentRange = {
32
- id: string;
33
- cursor: string; // TODO(burdon): Rename range.
34
- };
35
-
36
- // TODO(wittjosiah): Factor out to common package? @dxos/react-client?
37
- export type EditorModel = {
38
- id: string;
39
- text: () => string;
40
- extension?: Extension;
41
-
42
- // TODO(burdon): Remove.
43
- content: string | YText | YXmlFragment | DocAccessor;
44
-
45
- // TODO(burdon): Move into extension.
46
- awareness?: YP.Awareness;
47
-
48
- // TODO(burdon): Remove.
49
- peer?: {
50
- id: string;
51
- name?: string;
52
- };
53
- };
54
-
55
14
  // TODO(burdon): Remove space/identity dependency. Define interface for the framework re content and presence.
56
15
  export type UseTextModelProps = {
57
16
  identity?: Identity | null;
@@ -59,8 +18,6 @@ export type UseTextModelProps = {
59
18
  text?: TextObject;
60
19
  };
61
20
 
62
- // TODO(burdon): Remove YJS/Automerge deps (from UI component -- create abstraction; incl. all ECHO/Space deps).
63
- // TODO(wittjosiah): Factor out to common package? @dxos/react-client?
64
21
  export const useTextModel = (props: UseTextModelProps): EditorModel | undefined => {
65
22
  const { identity, space, text } = props;
66
23
  const [model, setModel] = useState<EditorModel | undefined>();
package/src/hooks/yjs.ts CHANGED
@@ -14,7 +14,8 @@ import { type Space } from '@dxos/react-client/echo';
14
14
  import { type GossipMessage } from '@dxos/react-client/mesh';
15
15
  import type { YText } from '@dxos/text-model';
16
16
 
17
- import { type EditorModel, modelState, type UseTextModelProps } from './useTextModel';
17
+ import { type EditorModel, modelState } from './defs';
18
+ import { type UseTextModelProps } from './useTextModel';
18
19
  import { yjs } from '../extensions';
19
20
 
20
21
  export const createYjsModel = ({ identity, space, text }: UseTextModelProps): EditorModel => {