@dxos/react-ui-editor 0.3.11-main.ec7a2b3 → 0.3.11-main.ee2b64c

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 (185) hide show
  1. package/dist/lib/browser/index.mjs +1702 -1100
  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/MarkdownEditor.stories.d.ts +13 -15
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts +14 -6
  7. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  8. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +1 -1
  9. package/dist/types/src/components/TextEditor/automerge.stories.d.ts.map +1 -1
  10. package/dist/types/src/components/TextEditor/{yjs.stories.d.ts → hooks.stories.d.ts} +13 -2
  11. package/dist/types/src/components/TextEditor/hooks.stories.d.ts.map +1 -0
  12. package/dist/types/src/components/Toolbar/Toolbar.d.ts +25 -0
  13. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -0
  14. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +23 -0
  15. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -0
  16. package/dist/types/src/components/Toolbar/index.d.ts +2 -0
  17. package/dist/types/src/components/Toolbar/index.d.ts.map +1 -0
  18. package/dist/types/src/components/index.d.ts +1 -0
  19. package/dist/types/src/components/index.d.ts.map +1 -1
  20. package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
  21. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  22. package/dist/types/src/extensions/automerge/automerge.spec.d.ts +2 -0
  23. package/dist/types/src/extensions/automerge/automerge.spec.d.ts.map +1 -0
  24. package/dist/types/src/extensions/automerge/automerge.test.d.ts +2 -0
  25. package/dist/types/src/extensions/automerge/automerge.test.d.ts.map +1 -0
  26. package/dist/types/src/extensions/automerge/defs.d.ts +9 -11
  27. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
  28. package/dist/types/src/extensions/automerge/index.d.ts +0 -1
  29. package/dist/types/src/extensions/automerge/index.d.ts.map +1 -1
  30. package/dist/types/src/extensions/automerge/semaphore.d.ts +8 -12
  31. package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -1
  32. package/dist/types/src/extensions/automerge/update-automerge.d.ts +2 -2
  33. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
  34. package/dist/types/src/extensions/awareness.d.ts +2 -2
  35. package/dist/types/src/extensions/awareness.d.ts.map +1 -1
  36. package/dist/types/src/extensions/basic.d.ts +4 -5
  37. package/dist/types/src/extensions/basic.d.ts.map +1 -1
  38. package/dist/types/src/extensions/comments.d.ts +35 -18
  39. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  40. package/dist/types/src/extensions/cursor.d.ts +3 -3
  41. package/dist/types/src/extensions/cursor.d.ts.map +1 -1
  42. package/dist/types/src/extensions/index.d.ts +1 -7
  43. package/dist/types/src/extensions/index.d.ts.map +1 -1
  44. package/dist/types/src/extensions/markdown/bundle.d.ts +3 -6
  45. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  46. package/dist/types/src/extensions/markdown/code.d.ts.map +1 -0
  47. package/dist/types/src/extensions/markdown/formatting.d.ts +13 -0
  48. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -0
  49. package/dist/types/src/extensions/markdown/heading.d.ts +6 -0
  50. package/dist/types/src/extensions/markdown/heading.d.ts.map +1 -0
  51. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
  52. package/dist/types/src/extensions/markdown/hr.d.ts.map +1 -0
  53. package/dist/types/src/extensions/markdown/image.d.ts.map +1 -0
  54. package/dist/types/src/extensions/markdown/index.d.ts +8 -0
  55. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
  56. package/dist/types/src/extensions/markdown/link.d.ts.map +1 -0
  57. package/dist/types/src/extensions/markdown/table.d.ts.map +1 -0
  58. package/dist/types/src/extensions/markdown/tasklist.d.ts.map +1 -0
  59. package/dist/types/src/extensions/mention.d.ts.map +1 -1
  60. package/dist/types/src/extensions/outliner.d.ts +4 -0
  61. package/dist/types/src/extensions/outliner.d.ts.map +1 -0
  62. package/dist/types/src/hooks/awareness-provider.d.ts +7 -4
  63. package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -1
  64. package/dist/types/src/hooks/defs.d.ts +25 -0
  65. package/dist/types/src/hooks/defs.d.ts.map +1 -0
  66. package/dist/types/src/hooks/index.d.ts +4 -2
  67. package/dist/types/src/hooks/index.d.ts.map +1 -1
  68. package/dist/types/src/hooks/useActionHandler.d.ts +4 -0
  69. package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -0
  70. package/dist/types/src/hooks/useEditorView.d.ts +17 -0
  71. package/dist/types/src/hooks/useEditorView.d.ts.map +1 -0
  72. package/dist/types/src/hooks/useTextEditor.d.ts +39 -0
  73. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -0
  74. package/dist/types/src/hooks/useTextModel.d.ts +14 -27
  75. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  76. package/dist/types/src/index.d.ts +1 -1
  77. package/dist/types/src/index.d.ts.map +1 -1
  78. package/dist/types/src/styles/markdown.d.ts +2 -2
  79. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  80. package/dist/types/src/themes/default.d.ts.map +1 -1
  81. package/dist/types/src/util.d.ts +2 -0
  82. package/dist/types/src/util.d.ts.map +1 -1
  83. package/package.json +48 -38
  84. package/src/components/TextEditor/MarkdownEditor.stories.tsx +73 -82
  85. package/src/components/TextEditor/TextEditor.tsx +111 -75
  86. package/src/components/TextEditor/automerge.stories.tsx +1 -2
  87. package/src/components/TextEditor/hooks.stories.tsx +106 -0
  88. package/src/components/Toolbar/Toolbar.stories.tsx +90 -0
  89. package/src/components/Toolbar/Toolbar.tsx +166 -0
  90. package/src/components/Toolbar/index.ts +5 -0
  91. package/src/components/index.ts +1 -0
  92. package/src/extensions/autocomplete.ts +6 -3
  93. package/src/extensions/automerge/automerge.spec.tsx +76 -0
  94. package/src/extensions/automerge/automerge.test.ts +13 -0
  95. package/src/extensions/automerge/automerge.ts +54 -45
  96. package/src/extensions/automerge/cursor.ts +3 -3
  97. package/src/extensions/automerge/defs.ts +13 -34
  98. package/src/extensions/automerge/index.ts +0 -1
  99. package/src/extensions/automerge/semaphore.ts +33 -32
  100. package/src/extensions/automerge/update-automerge.ts +14 -8
  101. package/src/extensions/automerge/update-codemirror.ts +5 -5
  102. package/src/extensions/awareness.ts +22 -18
  103. package/src/extensions/basic.ts +17 -10
  104. package/src/extensions/blast.ts +4 -1
  105. package/src/extensions/comments.ts +327 -166
  106. package/src/extensions/cursor.ts +7 -3
  107. package/src/extensions/index.ts +1 -7
  108. package/src/extensions/markdown/bundle.ts +23 -28
  109. package/src/extensions/markdown/code.ts +150 -0
  110. package/src/extensions/markdown/formatting.ts +188 -0
  111. package/src/extensions/markdown/heading.ts +59 -0
  112. package/src/extensions/markdown/highlight.ts +9 -15
  113. package/src/extensions/markdown/hr.ts +71 -0
  114. package/src/extensions/markdown/index.ts +8 -0
  115. package/src/extensions/{link.ts → markdown/link.ts} +16 -7
  116. package/src/extensions/markdown/tasklist.ts +110 -0
  117. package/src/extensions/mention.ts +9 -2
  118. package/src/extensions/outliner.ts +12 -0
  119. package/src/hooks/awareness-provider.ts +24 -20
  120. package/src/hooks/defs.ts +42 -0
  121. package/src/hooks/index.ts +5 -2
  122. package/src/hooks/useActionHandler.ts +62 -0
  123. package/src/hooks/useEditorView.ts +29 -0
  124. package/src/hooks/useTextEditor.ts +153 -0
  125. package/src/hooks/useTextModel.ts +66 -56
  126. package/src/index.ts +1 -1
  127. package/src/styles/markdown.ts +9 -10
  128. package/src/themes/default.ts +11 -8
  129. package/src/util.ts +26 -3
  130. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts +0 -12
  131. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts.map +0 -1
  132. package/dist/types/src/components/TextEditor/comments.stories.d.ts +0 -26
  133. package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +0 -1
  134. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
  135. package/dist/types/src/extensions/code.d.ts.map +0 -1
  136. package/dist/types/src/extensions/hr.d.ts.map +0 -1
  137. package/dist/types/src/extensions/image.d.ts.map +0 -1
  138. package/dist/types/src/extensions/link.d.ts.map +0 -1
  139. package/dist/types/src/extensions/table.d.ts.map +0 -1
  140. package/dist/types/src/extensions/tasklist.d.ts.map +0 -1
  141. package/dist/types/src/extensions/yjs/cursor.d.ts +0 -4
  142. package/dist/types/src/extensions/yjs/cursor.d.ts.map +0 -1
  143. package/dist/types/src/extensions/yjs/index.d.ts +0 -2
  144. package/dist/types/src/extensions/yjs/index.d.ts.map +0 -1
  145. package/dist/types/src/extensions/yjs/yjs.d.ts +0 -4
  146. package/dist/types/src/extensions/yjs/yjs.d.ts.map +0 -1
  147. package/dist/types/src/extensions/yjs/yjs.test.d.ts +0 -2
  148. package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +0 -1
  149. package/dist/types/src/hooks/automerge.d.ts +0 -3
  150. package/dist/types/src/hooks/automerge.d.ts.map +0 -1
  151. package/dist/types/src/hooks/yjs.d.ts +0 -27
  152. package/dist/types/src/hooks/yjs.d.ts.map +0 -1
  153. package/dist/types/src/testing/index.d.ts +0 -3
  154. package/dist/types/src/testing/index.d.ts.map +0 -1
  155. package/dist/types/src/testing/proto/gen/schema.d.ts +0 -15
  156. package/dist/types/src/testing/proto/gen/schema.d.ts.map +0 -1
  157. package/dist/types/src/testing/proto/index.d.ts +0 -2
  158. package/dist/types/src/testing/proto/index.d.ts.map +0 -1
  159. package/dist/types/src/testing/replicator.d.ts +0 -45
  160. package/dist/types/src/testing/replicator.d.ts.map +0 -1
  161. package/src/components/TextEditor/codemirror.stories.ts +0 -115
  162. package/src/components/TextEditor/comments.stories.tsx +0 -315
  163. package/src/components/TextEditor/yjs.stories.tsx +0 -56
  164. package/src/extensions/code.ts +0 -106
  165. package/src/extensions/hr.ts +0 -73
  166. package/src/extensions/tasklist.ts +0 -119
  167. package/src/extensions/yjs/cursor.ts +0 -22
  168. package/src/extensions/yjs/index.ts +0 -5
  169. package/src/extensions/yjs/yjs.test.ts +0 -35
  170. package/src/extensions/yjs/yjs.ts +0 -16
  171. package/src/hooks/automerge.ts +0 -52
  172. package/src/hooks/yjs.ts +0 -156
  173. package/src/testing/index.ts +0 -6
  174. package/src/testing/proto/gen/schema.ts +0 -49
  175. package/src/testing/proto/index.ts +0 -5
  176. package/src/testing/proto/schema.proto +0 -15
  177. package/src/testing/replicator.ts +0 -184
  178. /package/dist/types/src/extensions/{code.d.ts → markdown/code.d.ts} +0 -0
  179. /package/dist/types/src/extensions/{hr.d.ts → markdown/hr.d.ts} +0 -0
  180. /package/dist/types/src/extensions/{image.d.ts → markdown/image.d.ts} +0 -0
  181. /package/dist/types/src/extensions/{link.d.ts → markdown/link.d.ts} +0 -0
  182. /package/dist/types/src/extensions/{table.d.ts → markdown/table.d.ts} +0 -0
  183. /package/dist/types/src/extensions/{tasklist.d.ts → markdown/tasklist.d.ts} +0 -0
  184. /package/src/extensions/{image.ts → markdown/image.ts} +0 -0
  185. /package/src/extensions/{table.ts → markdown/table.ts} +0 -0
@@ -2,9 +2,9 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { EditorState, type Extension } from '@codemirror/state';
5
+ import { EditorState, type Extension, type StateEffect } from '@codemirror/state';
6
6
  import { EditorView } from '@codemirror/view';
7
- import { useFocusableGroup } from '@fluentui/react-tabster';
7
+ // import { useFocusableGroup } from '@fluentui/react-tabster';
8
8
  import { vim } from '@replit/codemirror-vim';
9
9
  import defaultsDeep from 'lodash.defaultsdeep';
10
10
  import React, {
@@ -14,19 +14,22 @@ import React, {
14
14
  useCallback,
15
15
  useEffect,
16
16
  useImperativeHandle,
17
- useState,
18
17
  useRef,
18
+ useState,
19
19
  } from 'react';
20
20
 
21
- import { generateName } from '@dxos/display-name';
21
+ import { log } from '@dxos/log';
22
22
  import { useThemeContext } from '@dxos/react-ui';
23
- import { getColorForValue, inputSurface, mx } from '@dxos/react-ui-theme';
23
+ import { focusRing, mx } from '@dxos/react-ui-theme';
24
+ import { isNotFalsy } from '@dxos/util';
24
25
 
25
- import { basicBundle, markdownBundle, setCommentRange } from '../../extensions';
26
- import { type CommentRange, type EditorModel } from '../../hooks';
26
+ import { createBasicBundle, createMarkdownExtensions } from '../../extensions';
27
+ import { type EditorModel } from '../../hooks';
27
28
  import { type ThemeStyles } from '../../styles';
28
29
  import { defaultTheme, markdownTheme, textTheme } from '../../themes';
30
+ import { logChanges } from '../../util';
29
31
 
32
+ // TODO(burdon): Change to enum?
30
33
  export const EditorModes = ['default', 'vim'] as const;
31
34
  export type EditorMode = (typeof EditorModes)[number];
32
35
 
@@ -41,23 +44,28 @@ export type CursorInfo = {
41
44
 
42
45
  export type TextEditorSlots = {
43
46
  root?: Omit<ComponentProps<'div'>, 'ref'>;
44
- // editor?: {
45
- // className?: string;
46
- // };
47
+ editor?: {
48
+ className?: string;
49
+ };
50
+ content?: {
51
+ className?: string;
52
+ };
47
53
  };
48
54
 
49
55
  // TODO(burdon): Spellcheck?
50
56
  export type TextEditorProps = {
51
- model: EditorModel;
52
- focus?: boolean;
53
- selection?: { anchor: number; head?: number };
57
+ model: EditorModel; // TODO(burdon): Optional (e.g., just provide content if readonly).
54
58
  readonly?: boolean; // TODO(burdon): Move into model.
55
- comments?: CommentRange[]; // TODO(burdon): Move into extension.
56
- extensions?: Extension[];
57
- editorMode?: EditorMode;
59
+ autoFocus?: boolean;
60
+ lineWrapping?: boolean;
61
+ scrollTo?: StateEffect<any>; // TODO(burdon): Restore scroll position: scrollTo EditorView.scrollSnapshot().
62
+ selection?: { anchor: number; head?: number };
63
+ editorMode?: EditorMode; // TODO(burdon): Factor out.
58
64
  placeholder?: string;
59
65
  theme?: ThemeStyles;
60
66
  slots?: TextEditorSlots;
67
+ extensions?: Extension[];
68
+ debug?: boolean;
61
69
  };
62
70
 
63
71
  /**
@@ -65,108 +73,134 @@ export type TextEditorProps = {
65
73
  */
66
74
  export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
67
75
  (
68
- { model, focus, selection, readonly, comments, extensions = [], editorMode, theme, slots = defaultSlots },
76
+ {
77
+ model,
78
+ readonly,
79
+ autoFocus,
80
+ scrollTo,
81
+ selection,
82
+ editorMode,
83
+ theme,
84
+ slots = defaultSlots,
85
+ extensions = [],
86
+ debug,
87
+ },
69
88
  forwardedRef,
70
89
  ) => {
71
- const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
90
+ // TODO(burdon): Hook causes error even if properties are not spread into div.
91
+ // Uncaught TypeError: Cannot read properties of undefined (reading 'relatedTarget')
92
+ // Uses event.detail, which is deprecated (not event.details). At runtime the event has a property `details`.
93
+ // https://github.com/microsoft/tabster/blob/master/src/State/FocusedElement.ts#L348 (e.detail.relatedTarget)
94
+ // https://github.com/microsoft/keyborg/blob/49e49b2c3ba0a5f6cc518ac46825d7551def8109/src/FocusEvent.ts#L58
95
+ // const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
72
96
  const { themeMode } = useThemeContext();
73
97
 
74
- // The editor view ref should only be used as an escape hatch.
75
98
  const rootRef = useRef<HTMLDivElement>(null);
76
99
  const [view, setView] = useState<EditorView | null>(null);
77
- useImperativeHandle<EditorView | null, EditorView | null>(forwardedRef, () => view, [view]);
78
-
79
- // Focus.
80
- useEffect(() => {
81
- if (view && focus) {
82
- view.focus();
83
- }
84
- }, [view, focus]);
85
100
 
86
- // TODO(burdon): Factor out as extension.
87
- const { awareness, peer } = model;
88
- useEffect(() => {
89
- if (awareness && peer) {
90
- awareness.setLocalStateField('user', {
91
- name: peer.name ?? generateName(peer.id),
92
- color: getColorForValue({ value: peer.id, type: 'color' }),
93
- colorLight: getColorForValue({ value: peer.id, themeMode, type: 'highlight' }),
94
- });
95
- }
96
- }, [awareness, peer, themeMode]);
101
+ // The view ref can be used to focus the editor.
102
+ // NOTE: This does not cause the parent to re-render, so the ref is not available immediately.
103
+ useImperativeHandle<EditorView | null, EditorView | null>(forwardedRef, () => view, [view]);
97
104
 
98
- // TODO(burdon): Factor out as extension.
105
+ // Set focus.
99
106
  useEffect(() => {
100
- if (view && comments !== undefined) {
101
- view.dispatch({
102
- effects: setCommentRange.of({ model, comments }),
103
- });
107
+ if (autoFocus && !view?.hasFocus) {
108
+ if (view?.state.selection.main?.from === 0) {
109
+ // Start at end of line.
110
+ const { to } = view.state.doc.lineAt(0);
111
+ view?.dispatch({ selection: { anchor: to } });
112
+ }
113
+ view?.focus();
104
114
  }
105
- }, [view, comments]);
115
+ }, [view, autoFocus]);
106
116
 
117
+ // Create editor state and view.
118
+ // The view is recreated if the model or extensions are changed.
107
119
  useEffect(() => {
108
120
  if (!model || !rootRef.current) {
109
121
  return;
110
122
  }
111
123
 
124
+ //
125
+ // EditorState
112
126
  // https://codemirror.net/docs/ref/#state.EditorStateConfig
127
+ //
113
128
  const state = EditorState.create({
114
129
  doc: model.text(),
115
- // TODO(burdon): Composer should store and set selection when switching documents.
116
130
  selection,
117
131
  extensions: [
118
- readonly && EditorState.readOnly.of(readonly),
119
-
120
- // TODO(burdon): Factor out VIM mode? (manage via MarkdownPlugin).
121
- editorMode === 'vim' && vim(),
132
+ // TODO(burdon): Doesn't catch errors in keymap functions.
133
+ EditorView.exceptionSink.of((err) => {
134
+ log.catch(err);
135
+ }),
122
136
 
123
137
  // Theme.
124
- // TODO(burdon): Make theme configurable.
138
+ // TODO(burdon): Make configurable.
125
139
  EditorView.baseTheme(defaultTheme),
126
140
  EditorView.theme(theme ?? {}),
127
141
  EditorView.darkTheme.of(themeMode === 'dark'),
142
+ EditorView.editorAttributes.of({ class: slots.editor?.className ?? '' }),
143
+ EditorView.contentAttributes.of({ class: slots.content?.className ?? '' }),
144
+
145
+ // State.
146
+ EditorView.editable.of(!readonly),
147
+ EditorState.readOnly.of(!!readonly),
128
148
 
129
149
  // Storage and replication.
150
+ // NOTE: This must come before user extensions.
130
151
  model.extension,
131
152
 
153
+ // TODO(burdon): Factor out? (Requires special handling for Escape/Enter below).
154
+ editorMode === 'vim' && vim(),
155
+
132
156
  // Custom.
133
157
  ...extensions,
134
- ].filter(Boolean) as Extension[],
158
+ ].filter(isNotFalsy),
135
159
  });
136
160
 
137
- // NOTE: This repaints the editor.
138
- // If the new state is derived from the old state, it will likely not be visible other than the cursor resetting.
139
- // Ideally this should not happen except when changing between text objects.
140
- view?.destroy();
161
+ //
162
+ // EditorView
163
+ // https://codemirror.net/docs/ref/#view.EditorViewConfig
164
+ //
141
165
  const newView = new EditorView({
142
- parent: rootRef.current,
143
166
  state,
144
- // NOTE: Uncomment to spy on all transactions.
167
+ parent: rootRef.current,
168
+ scrollTo,
169
+ // NOTE: Uncomment to debug/monitor all transactions.
145
170
  // https://codemirror.net/docs/ref/#view.EditorView.dispatch
146
- // dispatch: (transaction, view) => {
147
- // view.update([transaction]);
148
- // },
171
+ dispatchTransactions: (trs, view) => {
172
+ if (debug) {
173
+ logChanges(trs);
174
+ }
175
+ view.update(trs);
176
+ },
149
177
  });
150
178
 
179
+ view?.destroy();
151
180
  setView(newView);
181
+
152
182
  return () => {
153
183
  newView?.destroy();
154
184
  setView(null);
155
185
  };
156
186
  }, [rootRef, model, readonly, editorMode, themeMode]);
157
187
 
188
+ // Handles tab/focus.
189
+ // Pressing Escape focuses the outer div (to support tab navigation); pressing Enter refocuses the editor.
190
+ // TODO(burdon): Convert to keymap?
158
191
  const handleKeyUp = useCallback(
159
192
  (event: KeyboardEvent) => {
160
193
  const { key, altKey, shiftKey, metaKey, ctrlKey } = event;
161
194
  switch (key) {
162
- // TODO(burdon): Is this required (for vim mode?)
163
- // case 'Enter': {
164
- // view?.contentDOM.focus();
165
- // break;
166
- // }
195
+ case 'Enter': {
196
+ view?.focus();
197
+ break;
198
+ }
167
199
 
168
200
  case 'Escape': {
169
- editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey) && rootRef.current?.focus();
201
+ if (editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)) {
202
+ view?.focus();
203
+ }
170
204
  break;
171
205
  }
172
206
  }
@@ -178,25 +212,25 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
178
212
  <div
179
213
  key={model.id}
180
214
  role='none'
181
- ref={rootRef}
182
215
  tabIndex={0}
183
- onKeyUp={handleKeyUp}
184
216
  {...slots.root}
185
- {...(editorMode !== 'vim' && tabsterDOMAttribute)}
217
+ // {...(editorMode !== 'vim' && tabsterDOMAttribute)}
218
+ onKeyUp={handleKeyUp}
219
+ ref={rootRef}
186
220
  />
187
221
  );
188
222
  },
189
223
  );
190
224
 
191
225
  export const TextEditor = forwardRef<EditorView, TextEditorProps>(
192
- ({ readonly, placeholder, extensions = [], theme = textTheme, slots, ...props }, forwardedRef) => {
226
+ ({ readonly, placeholder, lineWrapping, theme = textTheme, slots, extensions = [], ...props }, forwardedRef) => {
193
227
  const { themeMode } = useThemeContext();
194
228
  const updatedSlots = defaultsDeep({}, slots, defaultTextSlots);
195
229
  return (
196
230
  <BaseTextEditor
197
231
  ref={forwardedRef}
198
232
  readonly={readonly}
199
- extensions={[basicBundle({ readonly, themeMode, placeholder }), ...extensions]}
233
+ extensions={[createBasicBundle({ themeMode, placeholder, lineWrapping }), ...extensions]}
200
234
  theme={theme}
201
235
  slots={updatedSlots}
202
236
  {...props}
@@ -205,16 +239,15 @@ export const TextEditor = forwardRef<EditorView, TextEditorProps>(
205
239
  },
206
240
  );
207
241
 
208
- // TODO(burdon): Remove (Just provide bundle, slots).
209
242
  export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
210
- ({ readonly, placeholder, extensions = [], theme = markdownTheme, slots, ...props }, forwardedRef) => {
243
+ ({ readonly, placeholder, theme = markdownTheme, slots, extensions = [], ...props }, forwardedRef) => {
211
244
  const { themeMode } = useThemeContext();
212
245
  const updatedSlots = defaultsDeep({}, slots, defaultMarkdownSlots);
213
246
  return (
214
247
  <BaseTextEditor
215
248
  ref={forwardedRef}
216
249
  readonly={readonly}
217
- extensions={[markdownBundle({ readonly, themeMode, placeholder }), ...extensions]}
250
+ extensions={[createMarkdownExtensions({ themeMode, placeholder }), ...extensions]}
218
251
  theme={theme}
219
252
  slots={updatedSlots}
220
253
  {...props}
@@ -225,7 +258,10 @@ export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
225
258
 
226
259
  export const defaultSlots: TextEditorSlots = {
227
260
  root: {
228
- className: mx('p-2 overflow-y-auto', inputSurface),
261
+ className: mx('grow', focusRing),
262
+ },
263
+ editor: {
264
+ className: 'bs-full',
229
265
  },
230
266
  };
231
267
 
@@ -13,7 +13,7 @@ import React, { useEffect, useRef, useState } from 'react';
13
13
 
14
14
  import { type Prop } from '@dxos/automerge/automerge';
15
15
  import { Repo, type DocHandle } from '@dxos/automerge/automerge-repo';
16
- import { Filter, setGlobalAutomergePreference } from '@dxos/echo-schema';
16
+ import { Filter } from '@dxos/echo-schema';
17
17
  import { type PublicKey } from '@dxos/keys';
18
18
  import { Expando, TextObject, useSpace } from '@dxos/react-client/echo';
19
19
  import { useIdentity } from '@dxos/react-client/halo';
@@ -130,7 +130,6 @@ const EchoStory = ({ id, spaceKey }: { id: number; spaceKey: PublicKey }) => {
130
130
 
131
131
  export const WithEcho = {
132
132
  render: () => {
133
- setGlobalAutomergePreference(true);
134
133
  return (
135
134
  <ClientRepeater
136
135
  count={2}
@@ -0,0 +1,106 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import '@dxosTheme';
6
+
7
+ import React from 'react';
8
+
9
+ import { useThemeContext } from '@dxos/react-ui';
10
+ import { fixedInsetFlexLayout, groupSurface, mx } from '@dxos/react-ui-theme';
11
+ import { withTheme } from '@dxos/storybook-utils';
12
+
13
+ import { TextEditor } from './TextEditor';
14
+ import {
15
+ code,
16
+ createMarkdownExtensions,
17
+ formatting,
18
+ heading,
19
+ hr,
20
+ image,
21
+ link,
22
+ table,
23
+ tasklist,
24
+ } from '../../extensions';
25
+ import { createDataExtensions, createThemeExtensions, useActionHandler, useTextEditor } from '../../hooks';
26
+ import { markdownTheme } from '../../themes';
27
+ import { Toolbar } from '../Toolbar';
28
+
29
+ // TODO(burdon): Demo toolbar with hooks.
30
+ // TODO(burdon): Build components from hooks and adapters for model/extensions, etc.
31
+ // TODO(burdon): Remove BaseTextEditor.
32
+ // TODO(burdon): Move scrolling container layout/logic into TextEditor/MarkdownEditor components.
33
+ // TODO(burdon): Define keymap in composer framework format.
34
+
35
+ type StoryProps = {
36
+ autoFocus?: boolean;
37
+ placeholder?: string;
38
+ doc?: string;
39
+ readonly?: boolean;
40
+ };
41
+
42
+ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
43
+ const { themeMode } = useThemeContext();
44
+ const { parentRef, view } = useTextEditor({
45
+ autoFocus,
46
+ doc,
47
+ extensions: [
48
+ //
49
+ createDataExtensions({ readonly }),
50
+ createThemeExtensions({
51
+ themeMode,
52
+ theme: markdownTheme,
53
+ slots: {
54
+ // TODO(burdon): Document classes re base theme.
55
+ // Semantic tokens (e.g., replacement of input surface).
56
+ editor: {
57
+ className: 'h-full p-4 bg-white text-black dark:bg-black dark:text-white',
58
+ },
59
+ },
60
+ }),
61
+ // TODO(burdon): Move lineWrapping.
62
+ createMarkdownExtensions({ placeholder, lineWrapping: true }),
63
+ // TODO(burdon): Move into markdown bundle (with React callbacks).
64
+ code(),
65
+ formatting(),
66
+ heading(),
67
+ hr(),
68
+ image(),
69
+ link(),
70
+ table(),
71
+ tasklist(),
72
+ ],
73
+ });
74
+
75
+ const handleAction = useActionHandler(view);
76
+
77
+ return (
78
+ <div className={mx(fixedInsetFlexLayout, groupSurface)}>
79
+ <div className='flex h-full justify-center'>
80
+ <div className='flex flex-col h-full w-[800px]'>
81
+ <Toolbar.Root onAction={handleAction}>
82
+ <Toolbar.Markdown />
83
+ </Toolbar.Root>
84
+
85
+ {/* TODO(burdon): Handle scrolling in component wrapper (like this). */}
86
+ <div role='none' className='h-full overflow-y-auto' ref={parentRef} />
87
+ </div>
88
+ </div>
89
+ </div>
90
+ );
91
+ };
92
+
93
+ export default {
94
+ title: 'react-ui-editor/useTextEditor',
95
+ component: TextEditor,
96
+ decorators: [withTheme],
97
+ render: (args: StoryProps) => <Story {...args} />,
98
+ };
99
+
100
+ export const Default = {
101
+ args: {
102
+ autoFocus: true,
103
+ placeholder: 'Text...',
104
+ doc: '# Demo\n\nThis is a document.\n\n',
105
+ },
106
+ };
@@ -0,0 +1,90 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import '@dxosTheme';
6
+
7
+ import { faker } from '@faker-js/faker';
8
+ import React, { type FC, useMemo, useState } from 'react';
9
+
10
+ import { TextObject } from '@dxos/echo-schema';
11
+ import { PublicKey } from '@dxos/keys';
12
+ import { fixedInsetFlexLayout, groupSurface, mx } from '@dxos/react-ui-theme';
13
+ import { withTheme } from '@dxos/storybook-utils';
14
+
15
+ import { Toolbar } from './Toolbar';
16
+ import { code, comments, formatting, heading, image, table, tasklist, useComments } from '../../extensions';
17
+ import { type Comment, useActionHandler, useEditorView, useTextModel } from '../../hooks';
18
+ import { MarkdownEditor } from '../TextEditor';
19
+
20
+ faker.seed(101);
21
+
22
+ const Story: FC<{ content: string }> = ({ content }) => {
23
+ const [item] = useState({ text: new TextObject(content) });
24
+ const [_comments, setComments] = useState<Comment[]>([]);
25
+ const [editorRef, editorView] = useEditorView();
26
+ const model = useTextModel({ text: item.text });
27
+ const extensions = useMemo(
28
+ () => [
29
+ code(),
30
+ comments({
31
+ onCreate: (cursor) => {
32
+ const id = PublicKey.random().toHex();
33
+ setComments((comments) => [...comments, { id, cursor }]);
34
+ return id;
35
+ },
36
+ }),
37
+ formatting(),
38
+ heading(),
39
+ image(),
40
+ table(),
41
+ tasklist(),
42
+ ],
43
+ [],
44
+ );
45
+
46
+ useComments(editorView, _comments);
47
+ const handleAction = useActionHandler(editorView);
48
+
49
+ if (!model) {
50
+ return null;
51
+ }
52
+
53
+ return (
54
+ <div className={mx(fixedInsetFlexLayout, groupSurface)}>
55
+ <div className='flex h-full justify-center'>
56
+ <div className='flex flex-col h-full w-[800px]'>
57
+ <Toolbar.Root onAction={handleAction}>
58
+ <Toolbar.Markdown />
59
+ <Toolbar.Separator />
60
+ <Toolbar.Extended />
61
+ </Toolbar.Root>
62
+ <MarkdownEditor ref={editorRef} model={model} extensions={extensions} />
63
+ </div>
64
+ </div>
65
+ </div>
66
+ );
67
+ };
68
+
69
+ export default {
70
+ title: 'react-ui-editor/Toolbar',
71
+ component: Toolbar,
72
+ render: (args: any) => <Story {...args} />,
73
+ decorators: [withTheme],
74
+ };
75
+
76
+ const content = [
77
+ 'Demo',
78
+ '',
79
+ 'The editor supports **Markdown** styles.',
80
+ '',
81
+ faker.lorem.paragraph({ min: 5, max: 8 }),
82
+ '',
83
+ '',
84
+ ].join('\n');
85
+
86
+ export const Default = {
87
+ args: {
88
+ content,
89
+ },
90
+ };