@dxos/react-ui-editor 0.6.5 → 0.6.6-staging.23d123d

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 (70) hide show
  1. package/dist/lib/browser/index.mjs +316 -324
  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/Toolbar/Toolbar.d.ts +5 -1
  5. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
  6. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +7 -0
  7. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
  8. package/dist/types/src/components/index.d.ts +0 -1
  9. package/dist/types/src/components/index.d.ts.map +1 -1
  10. package/dist/types/src/extensions/autocomplete.d.ts +2 -1
  11. package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
  12. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  13. package/dist/types/src/extensions/automerge/automerge.stories.d.ts +9 -2
  14. package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
  15. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
  16. package/dist/types/src/extensions/awareness/awareness-provider.d.ts.map +1 -1
  17. package/dist/types/src/extensions/awareness/awareness.d.ts +6 -6
  18. package/dist/types/src/extensions/awareness/awareness.d.ts.map +1 -1
  19. package/dist/types/src/extensions/comments.d.ts +1 -2
  20. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  21. package/dist/types/src/extensions/cursor.d.ts +1 -1
  22. package/dist/types/src/extensions/cursor.d.ts.map +1 -1
  23. package/dist/types/src/extensions/debug.d.ts +3 -0
  24. package/dist/types/src/extensions/debug.d.ts.map +1 -0
  25. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  26. package/dist/types/src/extensions/index.d.ts +1 -0
  27. package/dist/types/src/extensions/index.d.ts.map +1 -1
  28. package/dist/types/src/extensions/markdown/action.d.ts +1 -1
  29. package/dist/types/src/extensions/markdown/action.d.ts.map +1 -1
  30. package/dist/types/src/extensions/modes.d.ts +7 -4
  31. package/dist/types/src/extensions/modes.d.ts.map +1 -1
  32. package/dist/types/src/hooks/{useTextEditor.stories.d.ts → InputMode.stories.d.ts} +9 -9
  33. package/dist/types/src/hooks/InputMode.stories.d.ts.map +1 -0
  34. package/dist/types/src/{components/TextEditor → hooks}/TextEditor.stories.d.ts +8 -16
  35. package/dist/types/src/hooks/TextEditor.stories.d.ts.map +1 -0
  36. package/dist/types/src/hooks/useTextEditor.d.ts +20 -3
  37. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  38. package/dist/types/src/themes/default.d.ts.map +1 -1
  39. package/dist/types/src/translations.d.ts +4 -0
  40. package/dist/types/src/translations.d.ts.map +1 -1
  41. package/package.json +27 -27
  42. package/src/components/Toolbar/Toolbar.stories.tsx +18 -8
  43. package/src/components/Toolbar/Toolbar.tsx +93 -3
  44. package/src/components/index.ts +0 -1
  45. package/src/extensions/autocomplete.ts +3 -3
  46. package/src/extensions/automerge/automerge.stories.tsx +25 -18
  47. package/src/extensions/automerge/automerge.ts +2 -0
  48. package/src/extensions/automerge/cursor.ts +3 -4
  49. package/src/extensions/awareness/awareness-provider.ts +2 -0
  50. package/src/extensions/awareness/awareness.ts +34 -30
  51. package/src/extensions/comments.ts +7 -14
  52. package/src/extensions/cursor.ts +1 -1
  53. package/src/extensions/debug.ts +15 -0
  54. package/src/extensions/factories.ts +19 -13
  55. package/src/extensions/index.ts +1 -0
  56. package/src/extensions/markdown/action.ts +1 -0
  57. package/src/extensions/modes.ts +9 -6
  58. package/src/hooks/{useTextEditor.stories.tsx → InputMode.stories.tsx} +41 -47
  59. package/src/{components/TextEditor → hooks}/TextEditor.stories.tsx +24 -30
  60. package/src/hooks/useTextEditor.ts +75 -23
  61. package/src/themes/default.ts +20 -4
  62. package/src/translations.ts +4 -0
  63. package/dist/types/src/components/TextEditor/TextEditor.d.ts +0 -34
  64. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +0 -1
  65. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +0 -1
  66. package/dist/types/src/components/TextEditor/index.d.ts +0 -2
  67. package/dist/types/src/components/TextEditor/index.d.ts.map +0 -1
  68. package/dist/types/src/hooks/useTextEditor.stories.d.ts.map +0 -1
  69. package/src/components/TextEditor/TextEditor.tsx +0 -184
  70. package/src/components/TextEditor/index.ts +0 -5
@@ -54,6 +54,8 @@ export const automerge = (accessor: DocAccessor): Extension => {
54
54
 
55
55
  return [
56
56
  Cursor.converter.of(cursorConverter(accessor)),
57
+
58
+ // Track heads.
57
59
  syncState,
58
60
 
59
61
  // Reconcile external updates.
@@ -3,15 +3,14 @@
3
3
  //
4
4
 
5
5
  import { log } from '@dxos/log';
6
- import { toCursor, type DocAccessor, fromCursor } from '@dxos/react-client/echo';
6
+ import { type DocAccessor, fromCursor, toCursor } from '@dxos/react-client/echo';
7
7
 
8
8
  import { type CursorConverter } from '../cursor';
9
9
 
10
10
  export const cursorConverter = (accessor: DocAccessor): CursorConverter => ({
11
- // TODO(burdon): Handle assoc to associate with a previous character.
12
- toCursor: (pos) => {
11
+ toCursor: (pos, assoc) => {
13
12
  try {
14
- return toCursor(accessor, pos);
13
+ return toCursor(accessor, pos, assoc);
15
14
  } catch (err) {
16
15
  log.catch(err);
17
16
  return ''; // In case of invalid request (e.g., wrong document).
@@ -62,6 +62,8 @@ export class SpaceAwarenessProvider implements AwarenessProvider {
62
62
  state: this._localState,
63
63
  } satisfies ProtocolMessage);
64
64
 
65
+ // TODO(burdon): Replace with throttle.
66
+ // TODO(burdon): Send heads?
65
67
  await sleep(DEBOUNCE_INTERVAL);
66
68
  }
67
69
  });
@@ -51,10 +51,9 @@ export type AwarenessPosition = {
51
51
  };
52
52
 
53
53
  export type AwarenessInfo = {
54
- displayName?: string;
55
- // TODO(burdon): Rename light/dark.
56
- color?: string;
57
- lightColor?: string;
54
+ displayName: string;
55
+ darkColor: string;
56
+ lightColor: string;
58
57
  };
59
58
 
60
59
  export type AwarenessState = {
@@ -80,14 +79,14 @@ export const awareness = (provider = dummyProvider): Extension => {
80
79
  * Generates selection decorations from remote peers.
81
80
  */
82
81
  export class RemoteSelectionsDecorator implements PluginValue {
83
- public decorations: DecorationSet = RangeSet.of([]);
84
-
85
82
  private readonly _ctx = new Context();
83
+ private readonly _cursorConverter: CursorConverter;
84
+ private readonly _provider: AwarenessProvider;
85
+
86
+ private _lastAnchor?: number;
87
+ private _lastHead?: number;
86
88
 
87
- private _cursorConverter: CursorConverter;
88
- private _provider: AwarenessProvider;
89
- private _lastAnchor?: number = undefined;
90
- private _lastHead?: number = undefined;
89
+ public decorations: DecorationSet = RangeSet.of([]);
91
90
 
92
91
  constructor(view: EditorView) {
93
92
  this._cursorConverter = view.state.facet(Cursor.converter);
@@ -104,13 +103,13 @@ export class RemoteSelectionsDecorator implements PluginValue {
104
103
  }
105
104
 
106
105
  update(update: ViewUpdate) {
107
- this._updateLocalSelection(update);
108
- this._updateRemoteSelections(update);
106
+ this._updateLocalSelection(update.view);
107
+ this._updateRemoteSelections(update.view);
109
108
  }
110
109
 
111
- private _updateLocalSelection(update: ViewUpdate) {
112
- const hasFocus = update.view.hasFocus && update.view.dom.ownerDocument.hasFocus();
113
- const { anchor = undefined, head = undefined } = hasFocus ? update.state.selection.main : {};
110
+ private _updateLocalSelection(view: EditorView) {
111
+ const hasFocus = view.hasFocus && view.dom.ownerDocument.hasFocus();
112
+ const { anchor = undefined, head = undefined } = hasFocus ? view.state.selection.main : {};
114
113
  if (this._lastAnchor === anchor && this._lastHead === head) {
115
114
  return;
116
115
  }
@@ -122,14 +121,22 @@ export class RemoteSelectionsDecorator implements PluginValue {
122
121
  anchor !== undefined && head !== undefined
123
122
  ? {
124
123
  anchor: this._cursorConverter.toCursor(anchor),
125
- head: this._cursorConverter.toCursor(head),
124
+ head: this._cursorConverter.toCursor(head, -1),
126
125
  }
127
126
  : undefined,
128
127
  );
129
128
  }
130
129
 
131
- private _updateRemoteSelections(update: ViewUpdate) {
132
- const decorations: Range<Decoration>[] = [];
130
+ private _updateRemoteSelections(view: EditorView) {
131
+ const decorations: Range<Decoration>[] = [
132
+ // TODO(burdon): Factor out for testing.
133
+ // {
134
+ // from: 0,
135
+ // to: 0,
136
+ // value: Decoration.widget({ side: 0, block: false, widget: new RemoteCaretWidget('Test', 'red') }),
137
+ // },
138
+ ];
139
+
133
140
  const awarenessStates = this._provider.getRemoteStates();
134
141
  for (const state of awarenessStates) {
135
142
  const anchor = state.position?.anchor ? this._cursorConverter.fromCursor(state.position.anchor) : null;
@@ -138,15 +145,14 @@ export class RemoteSelectionsDecorator implements PluginValue {
138
145
  continue;
139
146
  }
140
147
 
141
- const start = Math.min(Math.min(anchor, head), update.view.state.doc.length);
142
- const end = Math.min(Math.max(anchor, head), update.view.state.doc.length);
148
+ const start = Math.min(Math.min(anchor, head), view.state.doc.length);
149
+ const end = Math.min(Math.max(anchor, head), view.state.doc.length);
143
150
 
144
- const startLine = update.view.state.doc.lineAt(start);
145
- const endLine = update.view.state.doc.lineAt(end);
151
+ const startLine = view.state.doc.lineAt(start);
152
+ const endLine = view.state.doc.lineAt(end);
146
153
 
147
- // TODO(burdon): Factor out styles.
148
- const color = state.info.color ?? '#30bced';
149
- const lightColor = state.info.lightColor ?? color + '33';
154
+ const darkColor = state.info.darkColor;
155
+ const lightColor = state.info.lightColor;
150
156
 
151
157
  if (startLine.number === endLine.number) {
152
158
  // Selected content in a single line.
@@ -180,7 +186,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
180
186
  });
181
187
 
182
188
  for (let i = startLine.number + 1; i < endLine.number; i++) {
183
- const linePos = update.view.state.doc.line(i).from;
189
+ const linePos = view.state.doc.line(i).from;
184
190
  decorations.push({
185
191
  from: linePos,
186
192
  to: linePos,
@@ -197,7 +203,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
197
203
  value: Decoration.widget({
198
204
  side: head - anchor > 0 ? -1 : 1, // The local cursor should be rendered outside the remote selection.
199
205
  block: false,
200
- widget: new RemoteCaretWidget(state.info.displayName ?? 'Anonymous', color),
206
+ widget: new RemoteCaretWidget(state.info.displayName ?? 'Anonymous', darkColor),
201
207
  }),
202
208
  });
203
209
  }
@@ -232,7 +238,6 @@ class RemoteCaretWidget extends WidgetType {
232
238
  span.appendChild(document.createTextNode('\u2060'));
233
239
  span.appendChild(info);
234
240
  span.appendChild(document.createTextNode('\u2060'));
235
-
236
241
  return span;
237
242
  }
238
243
 
@@ -296,12 +301,11 @@ const styles = EditorView.baseTheme({
296
301
  lineHeight: 'normal',
297
302
  userSelect: 'none',
298
303
  color: 'white',
299
- padding: '2px',
304
+ padding: '2px 6px',
300
305
  zIndex: 101,
301
306
  transition: 'opacity .3s ease-in-out',
302
307
  backgroundColor: 'inherit',
303
308
  borderRadius: '2px',
304
- // These should be separate.
305
309
  opacity: 0,
306
310
  transitionDelay: '0s',
307
311
  whiteSpace: 'nowrap',
@@ -25,7 +25,6 @@ import {
25
25
  import sortBy from 'lodash.sortby';
26
26
  import { useEffect, useMemo, useState } from 'react';
27
27
 
28
- import { type ThreadType } from '@braneframe/types';
29
28
  import { debounce, type UnsubscribeCallback } from '@dxos/async';
30
29
  import { log } from '@dxos/log';
31
30
  import { nonNullable } from '@dxos/util';
@@ -40,6 +39,7 @@ import { callbackWrapper } from '../util';
40
39
  // State management.
41
40
  //
42
41
 
42
+ // TODO(wittjosiah): Factor out, not comments-specific.
43
43
  const documentId = Facet.define<string | undefined, string | undefined>({ combine: (values) => values[0] });
44
44
 
45
45
  type CommentState = {
@@ -612,21 +612,16 @@ const hasActiveSelection = (state: EditorState): boolean => {
612
612
  };
613
613
 
614
614
  class ExternalCommentSync implements PluginValue {
615
- private unsubscribe: () => void;
615
+ private readonly unsubscribe: () => void;
616
616
 
617
617
  constructor(
618
618
  view: EditorView,
619
619
  id: string,
620
620
  subscribe: (sink: () => void) => UnsubscribeCallback,
621
- getThreads: () => ThreadType[],
621
+ getComments: () => Comment[],
622
622
  ) {
623
623
  const updateComments = () => {
624
- const threads = getThreads();
625
- const comments = threads
626
- .filter(nonNullable)
627
- .filter((thread) => thread.anchor)
628
- .map((thread) => ({ id: thread.id, cursor: thread.anchor! }));
629
-
624
+ const comments = getComments();
630
625
  if (id === view.state.facet(documentId)) {
631
626
  queueMicrotask(() => view.dispatch({ effects: setComments.of({ id, comments }) }));
632
627
  }
@@ -643,12 +638,12 @@ class ExternalCommentSync implements PluginValue {
643
638
  export const createExternalCommentSync = (
644
639
  id: string,
645
640
  subscribe: (sink: () => void) => UnsubscribeCallback,
646
- getThreads: () => ThreadType[],
641
+ getComments: () => Comment[],
647
642
  ): Extension =>
648
643
  ViewPlugin.fromClass(
649
644
  class {
650
645
  constructor(view: EditorView) {
651
- return new ExternalCommentSync(view, id, subscribe, getThreads);
646
+ return new ExternalCommentSync(view, id, subscribe, getComments);
652
647
  }
653
648
  },
654
649
  );
@@ -697,7 +692,7 @@ export const useComments = (view: EditorView | null | undefined, id: string, com
697
692
  * Hook provides an extension to listen for comment clicks and invoke a handler.
698
693
  */
699
694
  export const useCommentClickListener = (onCommentClick: (commentId: string) => void): Extension => {
700
- const observer = useMemo(
695
+ return useMemo(
701
696
  () =>
702
697
  EditorView.updateListener.of((update) => {
703
698
  update.transactions.forEach((transaction) => {
@@ -710,6 +705,4 @@ export const useCommentClickListener = (onCommentClick: (commentId: string) => v
710
705
  }),
711
706
  [onCommentClick],
712
707
  );
713
-
714
- return observer;
715
708
  };
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { type EditorState, Facet } from '@codemirror/state';
6
6
 
7
- import type { Range } from './types';
7
+ import { type Range } from './types';
8
8
 
9
9
  /**
10
10
  * Converts indexes into the text document into stable peer-independent cursors.
@@ -0,0 +1,15 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { syntaxTree } from '@codemirror/language';
6
+ import { type EditorState, type RangeSet, StateField, type Transaction } from '@codemirror/state';
7
+
8
+ // eslint-disable-next-line no-console
9
+ export const debugNodeLogger = (log: (...args: any[]) => void = console.log) => {
10
+ const logTokens = (state: EditorState) => syntaxTree(state).iterate({ enter: (node) => log(node.type) });
11
+ return StateField.define<any>({
12
+ create: (state) => logTokens(state),
13
+ update: (_: RangeSet<any>, tr: Transaction) => logTokens(tr.state),
14
+ });
15
+ };
@@ -170,24 +170,30 @@ export type DataExtensionsProps<T> = {
170
170
 
171
171
  // TODO(burdon): Move out of react-ui-editor (remove echo deps).
172
172
  export const createDataExtensions = <T>({ id, text, space, identity }: DataExtensionsProps<T>): Extension[] => {
173
- const extensions: Extension[] = text ? [automerge(text)] : [];
173
+ const extensions: Extension[] = [];
174
+ if (text) {
175
+ extensions.push(automerge(text));
176
+ }
174
177
 
175
178
  if (space && identity) {
176
179
  const peerId = identity?.identityKey.toHex();
177
180
  const { cursorLightValue, cursorDarkValue } =
178
181
  hueTokens[(identity?.profile?.data?.hue as HuePalette | undefined) ?? hexToHue(peerId ?? '0')];
179
- const awarenessProvider = new SpaceAwarenessProvider({
180
- space,
181
- channel: `awareness.${id}`,
182
- peerId: identity.identityKey.toHex(),
183
- info: {
184
- displayName: identity.profile?.displayName ?? generateName(identity.identityKey.toHex()),
185
- color: cursorDarkValue,
186
- lightColor: cursorLightValue,
187
- },
188
- });
189
-
190
- extensions.push(awareness(awarenessProvider));
182
+
183
+ extensions.push(
184
+ awareness(
185
+ new SpaceAwarenessProvider({
186
+ space,
187
+ channel: `awareness.${id}`,
188
+ peerId: identity.identityKey.toHex(),
189
+ info: {
190
+ displayName: identity.profile?.displayName ?? generateName(identity.identityKey.toHex()),
191
+ darkColor: cursorDarkValue,
192
+ lightColor: cursorLightValue,
193
+ },
194
+ }),
195
+ ),
196
+ );
191
197
  }
192
198
 
193
199
  return extensions;
@@ -10,6 +10,7 @@ export * from './blast';
10
10
  export * from './command';
11
11
  export * from './comments';
12
12
  export * from './cursor';
13
+ export * from './debug';
13
14
  export * from './doc';
14
15
  export * from './dnd';
15
16
  export * from './factories';
@@ -25,6 +25,7 @@ import {
25
25
  import { createComment } from '../comments';
26
26
 
27
27
  export type ActionType =
28
+ | 'view-mode'
28
29
  | 'blockquote'
29
30
  | 'strong'
30
31
  | 'codeblock'
@@ -9,28 +9,31 @@ import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
9
9
 
10
10
  export const focusEvent = 'focus.container';
11
11
 
12
- export type EditorMode = 'default' | 'vim' | 'vscode' | undefined;
12
+ export const EditorViewModes = ['preview', 'readonly', 'source'] as const;
13
+ export type EditorViewMode = (typeof EditorViewModes)[number];
14
+ export const EditorInputModes = ['default', 'vim', 'vscode'] as const;
15
+ export type EditorInputMode = (typeof EditorInputModes)[number];
13
16
 
14
- export type EditorConfig = {
17
+ export type EditorInputConfig = {
15
18
  type: string;
16
19
  noTabster?: boolean;
17
20
  };
18
21
 
19
- export const editorMode = Facet.define<EditorConfig, EditorConfig>({
22
+ export const editorInputMode = Facet.define<EditorInputConfig, EditorInputConfig>({
20
23
  combine: (modes) => modes[0] ?? {},
21
24
  });
22
25
 
23
- export const EditorModes: { [mode: string]: Extension } = {
26
+ export const InputModeExtensions: { [mode: string]: Extension } = {
24
27
  default: [],
25
28
  vscode: [
26
29
  // https://github.com/replit/codemirror-vscode-keymap
27
- editorMode.of({ type: 'vscode' }),
30
+ editorInputMode.of({ type: 'vscode' }),
28
31
  keymap.of(vscodeKeymap),
29
32
  ],
30
33
  vim: [
31
34
  // https://github.com/replit/codemirror-vim
32
35
  vim(),
33
- editorMode.of({ type: 'vim', noTabster: true }),
36
+ editorInputMode.of({ type: 'vim', noTabster: true }),
34
37
  keymap.of([
35
38
  {
36
39
  key: 'Alt-Escape',
@@ -8,41 +8,38 @@ import React, { useState } from 'react';
8
8
 
9
9
  import { Toolbar as NaturalToolbar, Select, useThemeContext, Tooltip } from '@dxos/react-ui';
10
10
  import { attentionSurface, mx, textBlockWidth } from '@dxos/react-ui-theme';
11
- import { withTheme } from '@dxos/storybook-utils';
11
+ import { withFullscreen, withTheme } from '@dxos/storybook-utils';
12
12
 
13
13
  import { useActionHandler } from './useActionHandler';
14
- import { useTextEditor } from './useTextEditor';
14
+ import { useTextEditor, type UseTextEditorProps } from './useTextEditor';
15
15
  import { Toolbar } from '../components';
16
- import { createBasicExtensions, createThemeExtensions } from '../extensions';
17
16
  import {
18
- type EditorMode,
19
- EditorModes,
17
+ type EditorInputMode,
20
18
  decorateMarkdown,
21
19
  createMarkdownExtensions,
22
20
  formattingKeymap,
21
+ image,
23
22
  table,
24
23
  useFormattingState,
25
- image,
24
+ createBasicExtensions,
25
+ createThemeExtensions,
26
+ InputModeExtensions,
26
27
  } from '../extensions';
27
28
  import translations from '../translations';
28
29
 
29
- type StoryProps = {
30
- autoFocus?: boolean;
31
- placeholder?: string;
32
- doc?: string;
33
- readonly?: boolean;
34
- };
30
+ type StoryProps = { placeholder?: string; readonly?: boolean } & UseTextEditorProps;
35
31
 
36
- const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
32
+ const Story = ({ autoFocus, initialValue, placeholder, readonly }: StoryProps) => {
37
33
  const { themeMode } = useThemeContext();
38
34
  const [formattingState, trackFormatting] = useFormattingState();
39
- const [editorMode, setEditorMode] = useState<EditorMode>('default');
35
+ const [editorInputMode, setEditorInputMode] = useState<EditorInputMode>('default');
40
36
  const { parentRef, view } = useTextEditor(
41
37
  () => ({
42
38
  autoFocus,
43
- doc,
39
+ initialValue,
40
+ moveToEndOfLine: true,
44
41
  extensions: [
45
- editorMode ? EditorModes[editorMode] : [],
42
+ editorInputMode ? InputModeExtensions[editorInputMode] : [],
46
43
  createBasicExtensions({ placeholder, lineWrapping: true, readonly }),
47
44
  createMarkdownExtensions({ themeMode }),
48
45
  createThemeExtensions({ themeMode }),
@@ -53,7 +50,7 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
53
50
  trackFormatting,
54
51
  ],
55
52
  }),
56
- [editorMode, themeMode, placeholder, readonly],
53
+ [editorInputMode, themeMode, placeholder, readonly],
57
54
  );
58
55
 
59
56
  const handleAction = useActionHandler(view);
@@ -62,10 +59,13 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
62
59
  // Also not sure if view is even guaranteed to exist at this point.
63
60
  return (
64
61
  <div role='none' className={mx('fixed inset-0 flex flex-col')}>
65
- <Toolbar.Root onAction={handleAction} state={formattingState} classNames={textBlockWidth}>
66
- <Toolbar.Markdown />
67
- <EditorModeToolbar editorMode={editorMode} setEditorMode={setEditorMode} />
68
- </Toolbar.Root>
62
+ <Tooltip.Provider>
63
+ <Toolbar.Root onAction={handleAction} state={formattingState} classNames={textBlockWidth}>
64
+ <Toolbar.Markdown />
65
+ <EditorInputModeToolbar editorInputMode={editorInputMode} setEditorInputMode={setEditorInputMode} />
66
+ </Toolbar.Root>
67
+ </Tooltip.Provider>
68
+
69
69
  <div role='none' className='grow overflow-hidden'>
70
70
  <div className={mx(textBlockWidth, attentionSurface)} ref={parentRef} />
71
71
  </div>
@@ -73,17 +73,17 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
73
73
  );
74
74
  };
75
75
 
76
- const EditorModeToolbar = ({
77
- editorMode,
78
- setEditorMode,
76
+ const EditorInputModeToolbar = ({
77
+ editorInputMode,
78
+ setEditorInputMode,
79
79
  }: {
80
- editorMode: EditorMode;
81
- setEditorMode: (mode: EditorMode) => void;
80
+ editorInputMode: EditorInputMode;
81
+ setEditorInputMode: (mode: EditorInputMode) => void;
82
82
  }) => {
83
83
  return (
84
- <Select.Root value={editorMode} onValueChange={(value) => setEditorMode(value as EditorMode)}>
84
+ <Select.Root value={editorInputMode} onValueChange={(value) => setEditorInputMode(value as EditorInputMode)}>
85
85
  <NaturalToolbar.Button asChild>
86
- <Select.TriggerButton variant='ghost'>{editorMode}</Select.TriggerButton>
86
+ <Select.TriggerButton variant='ghost'>{editorInputMode}</Select.TriggerButton>
87
87
  </NaturalToolbar.Button>
88
88
  <Select.Portal>
89
89
  <Select.Content>
@@ -104,30 +104,24 @@ const EditorModeToolbar = ({
104
104
  };
105
105
 
106
106
  export default {
107
- title: 'react-ui-editor/useTextEditor',
108
- decorators: [withTheme],
109
- render: (args: StoryProps) => (
110
- <Tooltip.Provider>
111
- <Story {...args} />
112
- </Tooltip.Provider>
113
- ),
107
+ title: 'react-ui-editor/InputMode',
108
+ decorators: [withTheme, withFullscreen()],
114
109
  parameters: { translations, layout: 'fullscreen' },
110
+ render: Story,
115
111
  };
116
112
 
117
113
  export const Default = {
118
- render: () => {
119
- const { parentRef } = useTextEditor();
120
- return <div className={mx(textBlockWidth, attentionSurface)} ref={parentRef} />;
121
- },
122
- };
123
-
124
- export const Basic = {
125
114
  render: () => {
126
115
  const { themeMode } = useThemeContext();
127
- const { parentRef } = useTextEditor(() => ({
128
- extensions: [createBasicExtensions({ placeholder: 'Enter text...' }), createThemeExtensions({ themeMode })],
129
- }));
130
- return <div className={mx(textBlockWidth, attentionSurface)} ref={parentRef} />;
116
+ const { parentRef } = useTextEditor({
117
+ extensions: [
118
+ //
119
+ createBasicExtensions({ placeholder: 'Enter text...' }),
120
+ createThemeExtensions({ themeMode }),
121
+ ],
122
+ });
123
+
124
+ return <div ref={parentRef} className={mx(textBlockWidth, attentionSurface, 'w-full')} />;
131
125
  },
132
126
  };
133
127
 
@@ -135,6 +129,6 @@ export const Markdown = {
135
129
  args: {
136
130
  autoFocus: true,
137
131
  placeholder: 'Text...',
138
- doc: '# Demo\n\nThis is a document.\n\n',
132
+ initialValue: '# Demo\n\nThis is a document.\n\n',
139
133
  },
140
134
  };
@@ -5,9 +5,9 @@
5
5
  import '@dxosTheme';
6
6
  import { markdown } from '@codemirror/lang-markdown';
7
7
  import { ArrowSquareOut, X } from '@phosphor-icons/react';
8
- import { type EditorView } from 'codemirror';
8
+ import { effect, useSignal } from '@preact/signals-react';
9
9
  import defaultsDeep from 'lodash.defaultsdeep';
10
- import React, { type FC, type KeyboardEvent, StrictMode, useMemo, useRef, useState } from 'react';
10
+ import React, { type FC, type KeyboardEvent, StrictMode, useMemo, useState } from 'react';
11
11
  import { createRoot } from 'react-dom/client';
12
12
 
13
13
  import { TextType } from '@braneframe/types';
@@ -21,9 +21,9 @@ import { Button, DensityProvider, Input, ThemeProvider, useThemeContext } from '
21
21
  import { baseSurface, defaultTx, getSize, mx, textBlockWidth } from '@dxos/react-ui-theme';
22
22
  import { withTheme } from '@dxos/storybook-utils';
23
23
 
24
- import { TextEditor, type TextEditorProps } from './TextEditor';
24
+ import { useTextEditor, type UseTextEditorProps } from './useTextEditor';
25
25
  import {
26
- EditorModes,
26
+ InputModeExtensions,
27
27
  annotations,
28
28
  autocomplete,
29
29
  blast,
@@ -32,6 +32,7 @@ import {
32
32
  comments,
33
33
  createBasicExtensions,
34
34
  createDataExtensions,
35
+ createExternalCommentSync,
35
36
  createMarkdownExtensions,
36
37
  createThemeExtensions,
37
38
  decorateMarkdown,
@@ -45,14 +46,13 @@ import {
45
46
  state,
46
47
  table,
47
48
  typewriter,
48
- useComments,
49
49
  type CommandAction,
50
50
  type CommandOptions,
51
51
  type Comment,
52
52
  type CommentsOptions,
53
53
  type SelectionState,
54
- } from '../../extensions';
55
- import translations from '../../translations';
54
+ } from '../extensions';
55
+ import translations from '../translations';
56
56
 
57
57
  faker.seed(101);
58
58
 
@@ -242,25 +242,19 @@ const renderLinkButton = (el: Element, url: string) => {
242
242
  type StoryProps = {
243
243
  id?: string;
244
244
  text?: string;
245
- comments?: Comment[];
246
245
  readonly?: boolean;
247
246
  placeholder?: string;
248
- } & Pick<TextEditorProps, 'selection' | 'extensions'>;
247
+ } & Pick<UseTextEditorProps, 'selection' | 'extensions'>;
249
248
 
250
249
  const Story = ({
251
250
  id = 'editor-' + PublicKey.random().toHex().slice(0, 8),
252
251
  text,
253
- comments,
254
252
  extensions: _extensions = [],
255
253
  readonly,
256
254
  placeholder = 'New document.',
257
- ...props
255
+ selection,
258
256
  }: StoryProps) => {
259
257
  const [object] = useState(createEchoObject(create(TextType, { content: text ?? '' })));
260
-
261
- const viewRef = useRef<EditorView>(null);
262
- useComments(viewRef.current, id, comments);
263
-
264
258
  const { themeMode } = useThemeContext();
265
259
  const extensions = useMemo(
266
260
  () => [
@@ -278,21 +272,16 @@ const Story = ({
278
272
  [_extensions, object],
279
273
  );
280
274
 
281
- return (
282
- <TextEditor
283
- {...props}
284
- id={id}
285
- ref={viewRef}
286
- doc={text}
287
- extensions={extensions}
288
- className={mx(textBlockWidth, 'min-bs-dvh')}
289
- />
275
+ const { parentRef, focusAttributes } = useTextEditor(
276
+ () => ({ id, initialValue: text, extensions, selection }),
277
+ [extensions],
290
278
  );
279
+
280
+ return <div role='none' ref={parentRef} className={mx(textBlockWidth, 'min-bs-dvh')} {...focusAttributes} />;
291
281
  };
292
282
 
293
283
  export default {
294
- title: 'react-ui-editor/TextEditor',
295
- component: TextEditor,
284
+ title: 'react-ui-editor/useTextEditor',
296
285
  decorators: [withTheme],
297
286
  render: Story,
298
287
  parameters: { translations, layout: 'fullscreen' },
@@ -474,17 +463,22 @@ export const Command = {
474
463
 
475
464
  export const Comments = {
476
465
  render: () => {
477
- const [_comments, setComments] = useState<Comment[]>([]);
466
+ const _comments = useSignal<Comment[]>([]);
478
467
  return (
479
468
  <Story
480
469
  text={str('# Comments', '', text.paragraphs, text.footer)}
481
- comments={_comments}
482
470
  extensions={[
471
+ createExternalCommentSync(
472
+ 'test',
473
+ (sink) => effect(() => sink()),
474
+ () => _comments.value,
475
+ ),
483
476
  comments({
477
+ id: 'test',
484
478
  onHover: onCommentsHover,
485
479
  onCreate: ({ cursor }) => {
486
480
  const id = PublicKey.random().toHex();
487
- setComments((commentRanges) => [...commentRanges, { id, cursor }]);
481
+ _comments.value = [..._comments.value, { id, cursor }];
488
482
  return id;
489
483
  },
490
484
  onSelect: (state) => {
@@ -511,7 +505,7 @@ export const Vim = {
511
505
  render: () => (
512
506
  <Story
513
507
  text={str('# Vim Mode', '', 'The distant future. The year 2000.', '', text.paragraphs)}
514
- extensions={[defaults, EditorModes.vim]}
508
+ extensions={[defaults, InputModeExtensions.vim]}
515
509
  />
516
510
  ),
517
511
  };