@dxos/react-ui-editor 0.3.11-main.2e1a8e1 → 0.3.11-main.2f1331c

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 (158) hide show
  1. package/dist/lib/browser/index.mjs +1365 -925
  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 +1 -2
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts +3 -2
  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/automerge/automerge.d.ts.map +1 -1
  21. package/dist/types/src/extensions/automerge/automerge.spec.d.ts +2 -0
  22. package/dist/types/src/extensions/automerge/automerge.spec.d.ts.map +1 -0
  23. package/dist/types/src/extensions/automerge/automerge.test.d.ts +2 -0
  24. package/dist/types/src/extensions/automerge/automerge.test.d.ts.map +1 -0
  25. package/dist/types/src/extensions/automerge/defs.d.ts +5 -5
  26. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
  27. package/dist/types/src/extensions/automerge/semaphore.d.ts +2 -1
  28. package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -1
  29. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
  30. package/dist/types/src/extensions/awareness.d.ts +2 -4
  31. package/dist/types/src/extensions/awareness.d.ts.map +1 -1
  32. package/dist/types/src/extensions/basic.d.ts +2 -2
  33. package/dist/types/src/extensions/basic.d.ts.map +1 -1
  34. package/dist/types/src/extensions/comments.d.ts +9 -5
  35. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  36. package/dist/types/src/extensions/index.d.ts +0 -7
  37. package/dist/types/src/extensions/index.d.ts.map +1 -1
  38. package/dist/types/src/extensions/markdown/bundle.d.ts +2 -2
  39. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  40. package/dist/types/src/extensions/markdown/code.d.ts.map +1 -0
  41. package/dist/types/src/extensions/markdown/formatting.d.ts +13 -0
  42. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -0
  43. package/dist/types/src/extensions/markdown/heading.d.ts +6 -0
  44. package/dist/types/src/extensions/markdown/heading.d.ts.map +1 -0
  45. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
  46. package/dist/types/src/extensions/markdown/hr.d.ts.map +1 -0
  47. package/dist/types/src/extensions/markdown/image.d.ts.map +1 -0
  48. package/dist/types/src/extensions/markdown/index.d.ts +8 -0
  49. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
  50. package/dist/types/src/extensions/markdown/link.d.ts.map +1 -0
  51. package/dist/types/src/extensions/markdown/table.d.ts.map +1 -0
  52. package/dist/types/src/extensions/markdown/tasklist.d.ts.map +1 -0
  53. package/dist/types/src/hooks/awareness-provider.d.ts +7 -4
  54. package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -1
  55. package/dist/types/src/hooks/defs.d.ts +1 -4
  56. package/dist/types/src/hooks/defs.d.ts.map +1 -1
  57. package/dist/types/src/hooks/index.d.ts +3 -2
  58. package/dist/types/src/hooks/index.d.ts.map +1 -1
  59. package/dist/types/src/hooks/useActionHandler.d.ts +4 -0
  60. package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -0
  61. package/dist/types/src/hooks/useEditorView.d.ts +17 -0
  62. package/dist/types/src/hooks/useEditorView.d.ts.map +1 -0
  63. package/dist/types/src/hooks/useTextEditor.d.ts +39 -0
  64. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -0
  65. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  66. package/dist/types/src/styles/markdown.d.ts +2 -2
  67. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  68. package/package.json +39 -29
  69. package/src/components/TextEditor/MarkdownEditor.stories.tsx +15 -19
  70. package/src/components/TextEditor/TextEditor.tsx +45 -25
  71. package/src/components/TextEditor/automerge.stories.tsx +1 -2
  72. package/src/components/TextEditor/comments.stories.tsx +8 -8
  73. package/src/components/TextEditor/hooks.stories.tsx +106 -0
  74. package/src/components/Toolbar/Toolbar.stories.tsx +90 -0
  75. package/src/components/Toolbar/Toolbar.tsx +166 -0
  76. package/src/components/Toolbar/index.ts +5 -0
  77. package/src/components/index.ts +1 -0
  78. package/src/extensions/automerge/automerge.spec.tsx +76 -0
  79. package/src/extensions/automerge/automerge.test.ts +13 -0
  80. package/src/extensions/automerge/automerge.ts +44 -10
  81. package/src/extensions/automerge/cursor.ts +3 -3
  82. package/src/extensions/automerge/defs.ts +9 -9
  83. package/src/extensions/automerge/semaphore.ts +17 -19
  84. package/src/extensions/automerge/update-automerge.ts +12 -6
  85. package/src/extensions/automerge/update-codemirror.ts +5 -5
  86. package/src/extensions/awareness.ts +17 -34
  87. package/src/extensions/basic.ts +17 -15
  88. package/src/extensions/comments.ts +126 -78
  89. package/src/extensions/index.ts +0 -7
  90. package/src/extensions/markdown/bundle.ts +9 -8
  91. package/src/extensions/markdown/code.ts +150 -0
  92. package/src/extensions/markdown/formatting.ts +188 -0
  93. package/src/extensions/markdown/heading.ts +59 -0
  94. package/src/extensions/markdown/highlight.ts +9 -15
  95. package/src/extensions/{hr.ts → markdown/hr.ts} +3 -4
  96. package/src/extensions/markdown/index.ts +8 -0
  97. package/src/extensions/{link.ts → markdown/link.ts} +5 -2
  98. package/src/extensions/{tasklist.ts → markdown/tasklist.ts} +2 -3
  99. package/src/hooks/awareness-provider.ts +24 -20
  100. package/src/hooks/defs.ts +1 -6
  101. package/src/hooks/index.ts +3 -2
  102. package/src/hooks/useActionHandler.ts +62 -0
  103. package/src/hooks/useEditorView.ts +29 -0
  104. package/src/hooks/useTextEditor.ts +153 -0
  105. package/src/hooks/useTextModel.ts +48 -11
  106. package/src/styles/markdown.ts +9 -10
  107. package/src/themes/default.ts +4 -4
  108. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts +0 -12
  109. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts.map +0 -1
  110. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
  111. package/dist/types/src/extensions/code.d.ts.map +0 -1
  112. package/dist/types/src/extensions/hr.d.ts.map +0 -1
  113. package/dist/types/src/extensions/image.d.ts.map +0 -1
  114. package/dist/types/src/extensions/link.d.ts.map +0 -1
  115. package/dist/types/src/extensions/table.d.ts.map +0 -1
  116. package/dist/types/src/extensions/tasklist.d.ts.map +0 -1
  117. package/dist/types/src/extensions/yjs/cursor.d.ts +0 -4
  118. package/dist/types/src/extensions/yjs/cursor.d.ts.map +0 -1
  119. package/dist/types/src/extensions/yjs/index.d.ts +0 -2
  120. package/dist/types/src/extensions/yjs/index.d.ts.map +0 -1
  121. package/dist/types/src/extensions/yjs/yjs.d.ts +0 -4
  122. package/dist/types/src/extensions/yjs/yjs.d.ts.map +0 -1
  123. package/dist/types/src/extensions/yjs/yjs.test.d.ts +0 -2
  124. package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +0 -1
  125. package/dist/types/src/hooks/automerge.d.ts +0 -4
  126. package/dist/types/src/hooks/automerge.d.ts.map +0 -1
  127. package/dist/types/src/hooks/yjs.d.ts +0 -28
  128. package/dist/types/src/hooks/yjs.d.ts.map +0 -1
  129. package/dist/types/src/testing/index.d.ts +0 -3
  130. package/dist/types/src/testing/index.d.ts.map +0 -1
  131. package/dist/types/src/testing/proto/gen/schema.d.ts +0 -15
  132. package/dist/types/src/testing/proto/gen/schema.d.ts.map +0 -1
  133. package/dist/types/src/testing/proto/index.d.ts +0 -2
  134. package/dist/types/src/testing/proto/index.d.ts.map +0 -1
  135. package/dist/types/src/testing/replicator.d.ts +0 -45
  136. package/dist/types/src/testing/replicator.d.ts.map +0 -1
  137. package/src/components/TextEditor/codemirror.stories.ts +0 -115
  138. package/src/components/TextEditor/yjs.stories.tsx +0 -56
  139. package/src/extensions/code.ts +0 -104
  140. package/src/extensions/yjs/cursor.ts +0 -22
  141. package/src/extensions/yjs/index.ts +0 -5
  142. package/src/extensions/yjs/yjs.test.ts +0 -35
  143. package/src/extensions/yjs/yjs.ts +0 -16
  144. package/src/hooks/automerge.ts +0 -53
  145. package/src/hooks/yjs.ts +0 -157
  146. package/src/testing/index.ts +0 -6
  147. package/src/testing/proto/gen/schema.ts +0 -49
  148. package/src/testing/proto/index.ts +0 -5
  149. package/src/testing/proto/schema.proto +0 -15
  150. package/src/testing/replicator.ts +0 -184
  151. /package/dist/types/src/extensions/{code.d.ts → markdown/code.d.ts} +0 -0
  152. /package/dist/types/src/extensions/{hr.d.ts → markdown/hr.d.ts} +0 -0
  153. /package/dist/types/src/extensions/{image.d.ts → markdown/image.d.ts} +0 -0
  154. /package/dist/types/src/extensions/{link.d.ts → markdown/link.d.ts} +0 -0
  155. /package/dist/types/src/extensions/{table.d.ts → markdown/table.d.ts} +0 -0
  156. /package/dist/types/src/extensions/{tasklist.d.ts → markdown/tasklist.d.ts} +0 -0
  157. /package/src/extensions/{image.ts → markdown/image.ts} +0 -0
  158. /package/src/extensions/{table.ts → markdown/table.ts} +0 -0
@@ -4,7 +4,7 @@
4
4
 
5
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, {
@@ -20,9 +20,10 @@ import React, {
20
20
 
21
21
  import { log } from '@dxos/log';
22
22
  import { useThemeContext } from '@dxos/react-ui';
23
- import { inputSurface, mx } from '@dxos/react-ui-theme';
23
+ import { attentionSurface, mx } from '@dxos/react-ui-theme';
24
+ import { isNotFalsy } from '@dxos/util';
24
25
 
25
- import { basicBundle, markdownBundle, useAwareness } from '../../extensions';
26
+ import { createBasicBundle, createMarkdownExtensions } from '../../extensions';
26
27
  import { type EditorModel } from '../../hooks';
27
28
  import { type ThemeStyles } from '../../styles';
28
29
  import { defaultTheme, markdownTheme, textTheme } from '../../themes';
@@ -53,10 +54,10 @@ export type TextEditorSlots = {
53
54
 
54
55
  // TODO(burdon): Spellcheck?
55
56
  export type TextEditorProps = {
56
- model: EditorModel;
57
+ model: EditorModel; // TODO(burdon): Optional (e.g., just provide content if readonly).
57
58
  readonly?: boolean; // TODO(burdon): Move into model.
58
- autofocus?: boolean;
59
- multiline?: boolean;
59
+ autoFocus?: boolean;
60
+ lineWrapping?: boolean;
60
61
  scrollTo?: StateEffect<any>; // TODO(burdon): Restore scroll position: scrollTo EditorView.scrollSnapshot().
61
62
  selection?: { anchor: number; head?: number };
62
63
  editorMode?: EditorMode; // TODO(burdon): Factor out.
@@ -64,6 +65,7 @@ export type TextEditorProps = {
64
65
  theme?: ThemeStyles;
65
66
  slots?: TextEditorSlots;
66
67
  extensions?: Extension[];
68
+ debug?: boolean;
67
69
  };
68
70
 
69
71
  /**
@@ -71,10 +73,26 @@ export type TextEditorProps = {
71
73
  */
72
74
  export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
73
75
  (
74
- { model, readonly, autofocus, scrollTo, selection, editorMode, theme, slots = defaultSlots, extensions = [] },
76
+ {
77
+ model,
78
+ readonly,
79
+ autoFocus,
80
+ scrollTo,
81
+ selection,
82
+ editorMode,
83
+ theme,
84
+ slots = defaultSlots,
85
+ extensions = [],
86
+ debug,
87
+ },
75
88
  forwardedRef,
76
89
  ) => {
77
- 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' });
78
96
  const { themeMode } = useThemeContext();
79
97
 
80
98
  const rootRef = useRef<HTMLDivElement>(null);
@@ -86,13 +104,15 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
86
104
 
87
105
  // Set focus.
88
106
  useEffect(() => {
89
- if (autofocus) {
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
+ }
90
113
  view?.focus();
91
114
  }
92
- }, [view, autofocus]);
93
-
94
- // Monitor awareness.
95
- useAwareness(model);
115
+ }, [view, autoFocus]);
96
116
 
97
117
  // Create editor state and view.
98
118
  // The view is recreated if the model or extensions are changed.
@@ -123,18 +143,19 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
123
143
  EditorView.contentAttributes.of({ class: slots.content?.className ?? '' }),
124
144
 
125
145
  // State.
146
+ EditorView.editable.of(!readonly),
126
147
  EditorState.readOnly.of(!!readonly),
127
148
 
128
149
  // Storage and replication.
129
150
  // NOTE: This must come before user extensions.
130
151
  model.extension,
131
152
 
132
- // TODO(burdon): Factor out (requires special handling for Escape/focus).
153
+ // TODO(burdon): Factor out? (Requires special handling for Escape/Enter below).
133
154
  editorMode === 'vim' && vim(),
134
155
 
135
156
  // Custom.
136
157
  ...extensions,
137
- ].filter(Boolean) as Extension[],
158
+ ].filter(isNotFalsy),
138
159
  });
139
160
 
140
161
  //
@@ -148,11 +169,10 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
148
169
  // NOTE: Uncomment to debug/monitor all transactions.
149
170
  // https://codemirror.net/docs/ref/#view.EditorView.dispatch
150
171
  dispatchTransactions: (trs, view) => {
151
- view.update(trs);
152
- const debug = false;
153
172
  if (debug) {
154
173
  logChanges(trs);
155
174
  }
175
+ view.update(trs);
156
176
  },
157
177
  });
158
178
 
@@ -167,6 +187,7 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
167
187
 
168
188
  // Handles tab/focus.
169
189
  // Pressing Escape focuses the outer div (to support tab navigation); pressing Enter refocuses the editor.
190
+ // TODO(burdon): Convert to keymap?
170
191
  const handleKeyUp = useCallback(
171
192
  (event: KeyboardEvent) => {
172
193
  const { key, altKey, shiftKey, metaKey, ctrlKey } = event;
@@ -178,7 +199,7 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
178
199
 
179
200
  case 'Escape': {
180
201
  if (editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)) {
181
- rootRef.current?.focus();
202
+ view?.focus();
182
203
  }
183
204
  break;
184
205
  }
@@ -192,25 +213,24 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
192
213
  key={model.id}
193
214
  role='none'
194
215
  tabIndex={0}
195
- onKeyUp={handleKeyUp}
196
216
  {...slots.root}
197
- {...(editorMode !== 'vim' && tabsterDOMAttribute)}
217
+ // {...(editorMode !== 'vim' && tabsterDOMAttribute)}
218
+ onKeyUp={handleKeyUp}
198
219
  ref={rootRef}
199
220
  />
200
221
  );
201
222
  },
202
223
  );
203
224
 
204
- // TODO(burdon): Single-line/scroll.
205
225
  export const TextEditor = forwardRef<EditorView, TextEditorProps>(
206
- ({ readonly, placeholder, multiline, theme = textTheme, slots, extensions = [], ...props }, forwardedRef) => {
226
+ ({ readonly, placeholder, lineWrapping, theme = textTheme, slots, extensions = [], ...props }, forwardedRef) => {
207
227
  const { themeMode } = useThemeContext();
208
228
  const updatedSlots = defaultsDeep({}, slots, defaultTextSlots);
209
229
  return (
210
230
  <BaseTextEditor
211
231
  ref={forwardedRef}
212
232
  readonly={readonly}
213
- extensions={[basicBundle({ themeMode, placeholder, multiline }), ...extensions]}
233
+ extensions={[createBasicBundle({ themeMode, placeholder, lineWrapping }), ...extensions]}
214
234
  theme={theme}
215
235
  slots={updatedSlots}
216
236
  {...props}
@@ -227,7 +247,7 @@ export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
227
247
  <BaseTextEditor
228
248
  ref={forwardedRef}
229
249
  readonly={readonly}
230
- extensions={[markdownBundle({ themeMode, readonly, placeholder }), ...extensions]}
250
+ extensions={[createMarkdownExtensions({ themeMode, placeholder }), ...extensions]}
231
251
  theme={theme}
232
252
  slots={updatedSlots}
233
253
  {...props}
@@ -239,7 +259,7 @@ export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
239
259
  export const defaultSlots: TextEditorSlots = {
240
260
  root: {
241
261
  // TODO(burdon): Add focusRing by default/as property?
242
- className: mx('flex flex-col grow overflow-y-auto', inputSurface),
262
+ className: mx('flex flex-col grow overflow-y-auto', attentionSurface),
243
263
  },
244
264
  editor: {
245
265
  className: 'h-full p-2',
@@ -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}
@@ -17,7 +17,7 @@ import { fixedInsetFlexLayout, mx } from '@dxos/react-ui-theme';
17
17
  import { withTheme } from '@dxos/storybook-utils';
18
18
 
19
19
  import { MarkdownEditor, TextEditor } from './TextEditor';
20
- import { comments, type CommentsOptions, setFocus, useComments } from '../../extensions';
20
+ import { comments, type CommentsOptions, focusComment, useComments } from '../../extensions';
21
21
  import { type Comment, type Range, useTextModel } from '../../hooks';
22
22
 
23
23
  faker.seed(101);
@@ -50,7 +50,7 @@ const Editor: FC<{
50
50
  if (!view.current?.hasFocus && selectedValue !== selected) {
51
51
  setSelected(selectedValue);
52
52
  if (selectedValue) {
53
- setFocus(view.current!, selectedValue);
53
+ focusComment(view.current!, selectedValue);
54
54
  }
55
55
  }
56
56
  }, [selected, commentRanges, selectedValue]);
@@ -150,10 +150,10 @@ const Thread: FC<{
150
150
  </div>
151
151
  ))}
152
152
 
153
- <div ref={containerRef} onClick={() => onSelect()} className='flex'>
153
+ <div ref={containerRef} className='flex' onClick={() => onSelect()}>
154
154
  <TextEditor
155
155
  ref={editorRef}
156
- autofocus={focus}
156
+ autoFocus={focus}
157
157
  model={model}
158
158
  placeholder={'Enter comment...'}
159
159
  slots={{ root: { className: 'grow rounded-b' } }}
@@ -241,7 +241,7 @@ const Story = ({ text, autoCreate }: StoryProps) => {
241
241
  : [],
242
242
  },
243
243
  ]);
244
- setSelected(id);
244
+ setSelected(id); // TODO(burdon): Not required.
245
245
  return id;
246
246
  };
247
247
 
@@ -273,8 +273,8 @@ const Story = ({ text, autoCreate }: StoryProps) => {
273
273
  );
274
274
  };
275
275
 
276
- const handleSelectComment: CommentsOptions['onSelect'] = ({ comments, selection: { active, closest } }) => {
277
- log.info('select', { active: active?.slice(0, 4), closest: closest?.slice(0, 4) });
276
+ const handleSelectComment: CommentsOptions['onSelect'] = ({ comments, selection: { current, closest } }) => {
277
+ log.info('select', { active: current?.slice(0, 4), closest: closest?.slice(0, 4) });
278
278
  setThreads((threads) =>
279
279
  threads.map((thread) => {
280
280
  const comment = comments.find(({ comment }) => comment.id === thread.id);
@@ -287,7 +287,7 @@ const Story = ({ text, autoCreate }: StoryProps) => {
287
287
  }),
288
288
  );
289
289
 
290
- setSelected(active ?? closest);
290
+ setSelected(current ?? closest);
291
291
  };
292
292
 
293
293
  const handleSelectThread = (id: string) => {
@@ -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
+ };
@@ -0,0 +1,166 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import {
6
+ type Icon,
7
+ At,
8
+ CaretRight,
9
+ ChatText,
10
+ Code,
11
+ Image,
12
+ Link,
13
+ ListBullets,
14
+ ListChecks,
15
+ ListNumbers,
16
+ TextStrikethrough,
17
+ Table,
18
+ TextB,
19
+ TextItalic,
20
+ } from '@phosphor-icons/react';
21
+ import { createContext } from '@radix-ui/react-context';
22
+ import React, { type PropsWithChildren } from 'react';
23
+
24
+ import { Button, type ButtonProps, DensityProvider, Select } from '@dxos/react-ui';
25
+ import { getSize } from '@dxos/react-ui-theme';
26
+
27
+ // TODO(burdon): Revert to string to make extensible?
28
+ export type ActionType =
29
+ | 'blockquote'
30
+ | 'bold'
31
+ | 'codeblock'
32
+ | 'comment'
33
+ | 'heading'
34
+ | 'image'
35
+ | 'italic'
36
+ | 'link'
37
+ | 'list'
38
+ | 'list-ordered'
39
+ | 'list-tasks'
40
+ | 'mention'
41
+ | 'prompt'
42
+ | 'strikethrough'
43
+ | 'table';
44
+
45
+ export type Action = {
46
+ type: ActionType;
47
+ data?: any;
48
+ };
49
+
50
+ export type ToolbarProps = PropsWithChildren<{
51
+ onAction?: (action: Action) => void;
52
+ }>;
53
+
54
+ const [ToolbarContextProvider, useToolbarContext] = createContext<ToolbarProps>('Toolbar');
55
+
56
+ const ToolbarRoot = ({ children, onAction }: ToolbarProps) => {
57
+ return (
58
+ <ToolbarContextProvider onAction={onAction}>
59
+ <DensityProvider density='fine'>
60
+ <div role='toolbar' className='flex w-full shrink-0 p-2 gap-4 items-center whitespace-nowrap overflow-hidden'>
61
+ {children}
62
+ </div>
63
+ </DensityProvider>
64
+ </ToolbarContextProvider>
65
+ );
66
+ };
67
+
68
+ type ToolbarButtonProps = {
69
+ Icon: Icon;
70
+ onClick: () => Action | undefined;
71
+ } & NonNullable<Pick<ButtonProps, 'title'>>;
72
+
73
+ const ToolbarButton = ({ Icon, onClick, title }: ToolbarButtonProps) => {
74
+ const { onAction } = useToolbarContext('ToolbarButton');
75
+ const handleClick = () => {
76
+ const action = onClick();
77
+ if (action) {
78
+ onAction?.(action);
79
+ }
80
+ };
81
+
82
+ return (
83
+ <Button variant='ghost' classNames='p-2' onClick={handleClick} title={title}>
84
+ <Icon className={getSize(5)} />
85
+ </Button>
86
+ );
87
+ };
88
+
89
+ const ToolbarSeparator = () => <div className='grow' />;
90
+
91
+ const MarkdownHeading = () => {
92
+ const { onAction } = useToolbarContext('MarkdownFormatting');
93
+ return (
94
+ <Select.Root defaultValue='0' onValueChange={(value) => onAction?.({ type: 'heading', data: parseInt(value) })}>
95
+ <Select.TriggerButton classNames='w-[8rem]' />
96
+ <Select.Portal>
97
+ <Select.Content>
98
+ <Select.Viewport>
99
+ <Select.Option value='0'>Paragraph</Select.Option>
100
+ {[1, 2, 3, 4, 5, 6].map((level) => (
101
+ <Select.Option key={level} value={String(level)}>
102
+ Heading {level}
103
+ </Select.Option>
104
+ ))}
105
+ </Select.Viewport>
106
+ </Select.Content>
107
+ </Select.Portal>
108
+ </Select.Root>
109
+ );
110
+ };
111
+
112
+ const MarkdownStyles = () => (
113
+ <div role='none'>
114
+ <ToolbarButton Icon={TextB} title='Bold' onClick={() => ({ type: 'bold' })} />
115
+ <ToolbarButton Icon={TextItalic} title='Italic' onClick={() => ({ type: 'italic' })} />
116
+ <ToolbarButton Icon={TextStrikethrough} title='Strike-through' onClick={() => ({ type: 'strikethrough' })} />
117
+ </div>
118
+ );
119
+
120
+ const MarkdownLists = () => (
121
+ <div role='none'>
122
+ <ToolbarButton Icon={ListBullets} title='Bullet list' onClick={() => ({ type: 'list' })} />
123
+ <ToolbarButton Icon={ListNumbers} title='Numbered list' onClick={() => ({ type: 'list-ordered' })} />
124
+ <ToolbarButton Icon={ListChecks} title='Task list' onClick={() => ({ type: 'list-tasks' })} />
125
+ </div>
126
+ );
127
+
128
+ const MarkdownBlocks = () => (
129
+ <div role='none'>
130
+ <ToolbarButton Icon={CaretRight} title='Block quite' onClick={() => ({ type: 'blockquote' })} />
131
+ <ToolbarButton Icon={Code} title='Code block' onClick={() => ({ type: 'codeblock' })} />
132
+ <ToolbarButton Icon={Table} title='Table' onClick={() => ({ type: 'table' })} />
133
+ </div>
134
+ );
135
+
136
+ const MarkdownLinks = () => (
137
+ <div role='none'>
138
+ <ToolbarButton Icon={Link} title='Link' onClick={() => ({ type: 'link' })} />
139
+ <ToolbarButton Icon={At} title='Mention' onClick={() => ({ type: 'mention' })} />
140
+ <ToolbarButton Icon={Image} title='Image' onClick={() => ({ type: 'image' })} />
141
+ </div>
142
+ );
143
+
144
+ const MarkdownStandard = () => (
145
+ <>
146
+ <MarkdownHeading />
147
+ <MarkdownStyles />
148
+ <MarkdownLists />
149
+ <MarkdownBlocks />
150
+ <MarkdownLinks />
151
+ </>
152
+ );
153
+
154
+ const MarkdownExtended = () => (
155
+ <Toolbar.Button Icon={ChatText} title='Create comment' onClick={() => ({ type: 'comment' })} />
156
+ );
157
+
158
+ export const Toolbar = {
159
+ Root: ToolbarRoot,
160
+ Button: ToolbarButton,
161
+ Separator: ToolbarSeparator,
162
+ Markdown: MarkdownStandard,
163
+ Extended: MarkdownExtended,
164
+ };
165
+
166
+ export { useToolbarContext };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './Toolbar';
@@ -3,3 +3,4 @@
3
3
  //
4
4
 
5
5
  export * from './TextEditor';
6
+ export * from './Toolbar';