@dxos/react-ui-editor 0.3.11-next.0fb359e → 0.3.11-next.e28df4f

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 (201) hide show
  1. package/dist/lib/browser/index.mjs +1854 -1391
  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 +16 -14
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts +20 -17
  7. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  8. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +6 -6
  9. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  10. package/dist/types/src/components/TextEditor/automerge.stories.d.ts.map +1 -1
  11. package/dist/types/src/components/TextEditor/comments.stories.d.ts +26 -0
  12. package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +1 -0
  13. package/dist/types/src/components/TextEditor/hooks.stories.d.ts +23 -0
  14. package/dist/types/src/components/TextEditor/hooks.stories.d.ts.map +1 -0
  15. package/dist/types/src/components/Toolbar/Toolbar.d.ts +25 -0
  16. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -0
  17. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +23 -0
  18. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -0
  19. package/dist/types/src/components/Toolbar/index.d.ts +2 -0
  20. package/dist/types/src/components/Toolbar/index.d.ts.map +1 -0
  21. package/dist/types/src/components/index.d.ts +1 -0
  22. package/dist/types/src/components/index.d.ts.map +1 -1
  23. package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
  24. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  25. package/dist/types/src/extensions/automerge/automerge.spec.d.ts +2 -0
  26. package/dist/types/src/extensions/automerge/automerge.spec.d.ts.map +1 -0
  27. package/dist/types/src/extensions/automerge/automerge.test.d.ts +2 -0
  28. package/dist/types/src/extensions/automerge/automerge.test.d.ts.map +1 -0
  29. package/dist/types/src/extensions/automerge/cursor.d.ts +1 -1
  30. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
  31. package/dist/types/src/extensions/automerge/defs.d.ts +9 -11
  32. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
  33. package/dist/types/src/extensions/automerge/semaphore.d.ts +8 -12
  34. package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -1
  35. package/dist/types/src/extensions/automerge/update-automerge.d.ts +2 -2
  36. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
  37. package/dist/types/src/extensions/awareness.d.ts +2 -2
  38. package/dist/types/src/extensions/awareness.d.ts.map +1 -1
  39. package/dist/types/src/extensions/basic.d.ts +4 -5
  40. package/dist/types/src/extensions/basic.d.ts.map +1 -1
  41. package/dist/types/src/extensions/comments.d.ts +37 -20
  42. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  43. package/dist/types/src/{util → extensions}/cursor.d.ts +10 -2
  44. package/dist/types/src/extensions/cursor.d.ts.map +1 -0
  45. package/dist/types/src/extensions/index.d.ts +2 -8
  46. package/dist/types/src/extensions/index.d.ts.map +1 -1
  47. package/dist/types/src/extensions/markdown/bundle.d.ts +3 -6
  48. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  49. package/dist/types/src/extensions/markdown/code.d.ts.map +1 -0
  50. package/dist/types/src/extensions/markdown/formatting.d.ts +13 -0
  51. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -0
  52. package/dist/types/src/extensions/markdown/heading.d.ts +6 -0
  53. package/dist/types/src/extensions/markdown/heading.d.ts.map +1 -0
  54. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
  55. package/dist/types/src/extensions/markdown/hr.d.ts.map +1 -0
  56. package/dist/types/src/extensions/markdown/image.d.ts.map +1 -0
  57. package/dist/types/src/extensions/markdown/index.d.ts +8 -0
  58. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
  59. package/dist/types/src/extensions/markdown/link.d.ts.map +1 -0
  60. package/dist/types/src/extensions/markdown/table.d.ts.map +1 -0
  61. package/dist/types/src/extensions/markdown/tasklist.d.ts.map +1 -0
  62. package/dist/types/src/extensions/mention.d.ts.map +1 -1
  63. package/dist/types/src/extensions/outliner.d.ts +4 -0
  64. package/dist/types/src/extensions/outliner.d.ts.map +1 -0
  65. package/dist/types/src/hooks/awareness-provider.d.ts +7 -4
  66. package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -1
  67. package/dist/types/src/hooks/defs.d.ts +25 -0
  68. package/dist/types/src/hooks/defs.d.ts.map +1 -0
  69. package/dist/types/src/hooks/index.d.ts +4 -2
  70. package/dist/types/src/hooks/index.d.ts.map +1 -1
  71. package/dist/types/src/hooks/useActionHandler.d.ts +4 -0
  72. package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -0
  73. package/dist/types/src/hooks/useEditorView.d.ts +17 -0
  74. package/dist/types/src/hooks/useEditorView.d.ts.map +1 -0
  75. package/dist/types/src/hooks/useTextEditor.d.ts +39 -0
  76. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -0
  77. package/dist/types/src/hooks/useTextModel.d.ts +2 -32
  78. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  79. package/dist/types/src/index.d.ts +3 -0
  80. package/dist/types/src/index.d.ts.map +1 -1
  81. package/dist/types/src/styles/markdown.d.ts +2 -2
  82. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  83. package/dist/types/src/styles/tokens.d.ts +1 -0
  84. package/dist/types/src/styles/tokens.d.ts.map +1 -1
  85. package/dist/types/src/themes/default.d.ts.map +1 -1
  86. package/dist/types/src/util.d.ts +7 -0
  87. package/dist/types/src/util.d.ts.map +1 -0
  88. package/package.json +48 -37
  89. package/src/components/TextEditor/MarkdownEditor.stories.tsx +84 -91
  90. package/src/components/TextEditor/TextEditor.stories.tsx +12 -46
  91. package/src/components/TextEditor/TextEditor.tsx +136 -95
  92. package/src/components/TextEditor/automerge.stories.tsx +1 -2
  93. package/src/components/TextEditor/comments.stories.tsx +357 -0
  94. package/src/components/TextEditor/hooks.stories.tsx +106 -0
  95. package/src/components/Toolbar/Toolbar.stories.tsx +90 -0
  96. package/src/components/Toolbar/Toolbar.tsx +166 -0
  97. package/src/components/Toolbar/index.ts +5 -0
  98. package/src/components/index.ts +1 -0
  99. package/src/extensions/autocomplete.ts +23 -19
  100. package/src/extensions/automerge/automerge.spec.tsx +76 -0
  101. package/src/extensions/automerge/automerge.test.ts +13 -0
  102. package/src/extensions/automerge/automerge.ts +55 -46
  103. package/src/extensions/automerge/cursor.ts +5 -4
  104. package/src/extensions/automerge/defs.ts +13 -34
  105. package/src/extensions/automerge/semaphore.ts +33 -32
  106. package/src/extensions/automerge/update-automerge.ts +14 -8
  107. package/src/extensions/automerge/update-codemirror.ts +5 -5
  108. package/src/extensions/awareness.ts +43 -49
  109. package/src/extensions/basic.ts +17 -10
  110. package/src/extensions/comments.ts +323 -268
  111. package/src/extensions/cursor.ts +50 -0
  112. package/src/extensions/index.ts +2 -8
  113. package/src/extensions/markdown/bundle.ts +23 -28
  114. package/src/extensions/markdown/code.ts +150 -0
  115. package/src/extensions/markdown/formatting.ts +188 -0
  116. package/src/extensions/markdown/heading.ts +59 -0
  117. package/src/extensions/markdown/highlight.ts +11 -26
  118. package/src/extensions/markdown/hr.ts +71 -0
  119. package/src/extensions/markdown/image.ts +87 -0
  120. package/src/extensions/markdown/index.ts +8 -0
  121. package/src/extensions/{link.ts → markdown/link.ts} +87 -86
  122. package/src/extensions/{table.ts → markdown/table.ts} +10 -4
  123. package/src/extensions/markdown/tasklist.ts +110 -0
  124. package/src/extensions/mention.ts +9 -2
  125. package/src/extensions/outliner.ts +12 -0
  126. package/src/hooks/awareness-provider.ts +24 -20
  127. package/src/hooks/defs.ts +42 -0
  128. package/src/hooks/index.ts +5 -2
  129. package/src/hooks/useActionHandler.ts +62 -0
  130. package/src/hooks/useEditorView.ts +29 -0
  131. package/src/hooks/useTextEditor.ts +153 -0
  132. package/src/hooks/useTextModel.ts +50 -59
  133. package/src/index.ts +3 -0
  134. package/src/styles/markdown.ts +9 -12
  135. package/src/styles/tokens.ts +3 -0
  136. package/src/themes/default.ts +53 -80
  137. package/src/util.ts +41 -0
  138. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts +0 -12
  139. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts.map +0 -1
  140. package/dist/types/src/components/TextEditor/yjs.stories.d.ts +0 -12
  141. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
  142. package/dist/types/src/extensions/code.d.ts.map +0 -1
  143. package/dist/types/src/extensions/hr.d.ts.map +0 -1
  144. package/dist/types/src/extensions/image.d.ts.map +0 -1
  145. package/dist/types/src/extensions/link.d.ts.map +0 -1
  146. package/dist/types/src/extensions/mermaid.d.ts +0 -3
  147. package/dist/types/src/extensions/mermaid.d.ts.map +0 -1
  148. package/dist/types/src/extensions/table.d.ts.map +0 -1
  149. package/dist/types/src/extensions/tasklist.d.ts.map +0 -1
  150. package/dist/types/src/extensions/yjs/cursor.d.ts +0 -4
  151. package/dist/types/src/extensions/yjs/cursor.d.ts.map +0 -1
  152. package/dist/types/src/extensions/yjs/index.d.ts +0 -2
  153. package/dist/types/src/extensions/yjs/index.d.ts.map +0 -1
  154. package/dist/types/src/extensions/yjs/yjs.d.ts +0 -4
  155. package/dist/types/src/extensions/yjs/yjs.d.ts.map +0 -1
  156. package/dist/types/src/extensions/yjs/yjs.test.d.ts +0 -2
  157. package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +0 -1
  158. package/dist/types/src/hooks/automerge.d.ts +0 -3
  159. package/dist/types/src/hooks/automerge.d.ts.map +0 -1
  160. package/dist/types/src/hooks/yjs.d.ts +0 -27
  161. package/dist/types/src/hooks/yjs.d.ts.map +0 -1
  162. package/dist/types/src/testing/index.d.ts +0 -3
  163. package/dist/types/src/testing/index.d.ts.map +0 -1
  164. package/dist/types/src/testing/proto/gen/schema.d.ts +0 -15
  165. package/dist/types/src/testing/proto/gen/schema.d.ts.map +0 -1
  166. package/dist/types/src/testing/proto/index.d.ts +0 -2
  167. package/dist/types/src/testing/proto/index.d.ts.map +0 -1
  168. package/dist/types/src/testing/replicator.d.ts +0 -45
  169. package/dist/types/src/testing/replicator.d.ts.map +0 -1
  170. package/dist/types/src/util/cursor.d.ts.map +0 -1
  171. package/dist/types/src/util/index.d.ts +0 -3
  172. package/dist/types/src/util/index.d.ts.map +0 -1
  173. package/dist/types/src/util/util.d.ts +0 -5
  174. package/dist/types/src/util/util.d.ts.map +0 -1
  175. package/src/components/TextEditor/codemirror.stories.ts +0 -115
  176. package/src/components/TextEditor/yjs.stories.tsx +0 -56
  177. package/src/extensions/code.ts +0 -99
  178. package/src/extensions/hr.ts +0 -74
  179. package/src/extensions/image.ts +0 -69
  180. package/src/extensions/mermaid.ts +0 -11
  181. package/src/extensions/tasklist.ts +0 -121
  182. package/src/extensions/yjs/cursor.ts +0 -21
  183. package/src/extensions/yjs/index.ts +0 -5
  184. package/src/extensions/yjs/yjs.test.ts +0 -35
  185. package/src/extensions/yjs/yjs.ts +0 -16
  186. package/src/hooks/automerge.ts +0 -52
  187. package/src/hooks/yjs.ts +0 -178
  188. package/src/testing/index.ts +0 -6
  189. package/src/testing/proto/gen/schema.ts +0 -49
  190. package/src/testing/proto/index.ts +0 -5
  191. package/src/testing/proto/schema.proto +0 -15
  192. package/src/testing/replicator.ts +0 -184
  193. package/src/util/cursor.ts +0 -29
  194. package/src/util/index.ts +0 -6
  195. package/src/util/util.ts +0 -18
  196. /package/dist/types/src/extensions/{code.d.ts → markdown/code.d.ts} +0 -0
  197. /package/dist/types/src/extensions/{hr.d.ts → markdown/hr.d.ts} +0 -0
  198. /package/dist/types/src/extensions/{image.d.ts → markdown/image.d.ts} +0 -0
  199. /package/dist/types/src/extensions/{link.d.ts → markdown/link.d.ts} +0 -0
  200. /package/dist/types/src/extensions/{table.d.ts → markdown/table.d.ts} +0 -0
  201. /package/dist/types/src/extensions/{tasklist.d.ts → markdown/tasklist.d.ts} +0 -0
@@ -0,0 +1,357 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import '@dxosTheme';
6
+
7
+ import { type EditorView, keymap } from '@codemirror/view';
8
+ import { faker } from '@faker-js/faker';
9
+ import { Check, Trash } from '@phosphor-icons/react';
10
+ import React, { type FC, useEffect, useMemo, useRef, useState } from 'react';
11
+
12
+ import { getTextContent, TextObject } from '@dxos/echo-schema';
13
+ import { PublicKey } from '@dxos/keys';
14
+ import { log } from '@dxos/log';
15
+ import { Button, DensityProvider } from '@dxos/react-ui';
16
+ import { fixedInsetFlexLayout, mx } from '@dxos/react-ui-theme';
17
+ import { withTheme } from '@dxos/storybook-utils';
18
+
19
+ import { MarkdownEditor, TextEditor } from './TextEditor';
20
+ import { comments, type CommentsOptions, focusComment, useComments } from '../../extensions';
21
+ import { type Comment, type Range, useTextModel } from '../../hooks';
22
+
23
+ faker.seed(101);
24
+
25
+ //
26
+ // Editor
27
+ //
28
+
29
+ const Editor: FC<{
30
+ item: { text: TextObject };
31
+ comments: Comment[];
32
+ selected?: string;
33
+ onCreateComment: CommentsOptions['onCreate'];
34
+ onDeleteComment: CommentsOptions['onDelete'];
35
+ onUpdateComment: CommentsOptions['onUpdate'];
36
+ onSelectComment: CommentsOptions['onSelect'];
37
+ }> = ({
38
+ item,
39
+ selected: selectedValue,
40
+ comments: commentRanges,
41
+ onCreateComment,
42
+ onDeleteComment,
43
+ onUpdateComment,
44
+ onSelectComment,
45
+ }) => {
46
+ const model = useTextModel({ text: item.text });
47
+ const view = useRef<EditorView>(null);
48
+ const [selected, setSelected] = useState<string>();
49
+ useEffect(() => {
50
+ if (!view.current?.hasFocus && selectedValue !== selected) {
51
+ setSelected(selectedValue);
52
+ if (selectedValue) {
53
+ focusComment(view.current!, selectedValue);
54
+ }
55
+ }
56
+ }, [selected, commentRanges, selectedValue]);
57
+
58
+ useComments(view.current, commentRanges);
59
+
60
+ const extensions = useMemo(() => {
61
+ return [
62
+ comments({
63
+ onCreate: onCreateComment,
64
+ onDelete: onDeleteComment,
65
+ onUpdate: onUpdateComment,
66
+ onSelect: onSelectComment,
67
+ }),
68
+ ];
69
+ }, []);
70
+
71
+ if (!model) {
72
+ return null;
73
+ }
74
+
75
+ // TODO(burdon): Highlight currently selected comment.
76
+ return (
77
+ <div className='flex grow overflow-y-scroll'>
78
+ <MarkdownEditor ref={view} model={model} extensions={extensions} />
79
+ </div>
80
+ );
81
+ };
82
+
83
+ //
84
+ // Comments thread
85
+ //
86
+
87
+ type CommentThread = {
88
+ id: string;
89
+ cursor?: string;
90
+ range?: Range;
91
+ yPos?: number;
92
+ messages: TextObject[];
93
+ };
94
+
95
+ const Thread: FC<{
96
+ thread: CommentThread;
97
+ selected: boolean;
98
+ onSelect: () => void;
99
+ onResolve: () => void;
100
+ }> = ({ thread, selected, onSelect, onResolve }) => {
101
+ const [focus, setFocus] = useState(false);
102
+ const [item, setItem] = useState({ text: new TextObject() });
103
+ const model = useTextModel({ text: item.text });
104
+ const editorRef = useRef<EditorView>(null);
105
+
106
+ const containerRef = useRef<HTMLDivElement>(null);
107
+ useEffect(() => {
108
+ if (selected) {
109
+ containerRef.current?.scrollIntoView({ block: 'center', behavior: 'smooth' });
110
+ if (thread.messages.length === 0) {
111
+ setFocus(true);
112
+ }
113
+ }
114
+ }, [selected]);
115
+
116
+ const handleCreateMessage = () => {
117
+ const text = model?.text().trim();
118
+ if (text?.length) {
119
+ thread.messages.push(item.text);
120
+ setItem({ text: new TextObject() });
121
+ setFocus(true);
122
+ }
123
+ };
124
+
125
+ if (!model) {
126
+ return null;
127
+ }
128
+
129
+ return (
130
+ <div
131
+ className={mx(
132
+ 'flex flex-col m-1 rounded shadow divide-y bg-white',
133
+ selected && 'ring',
134
+ !thread.cursor && 'opacity-50',
135
+ )}
136
+ >
137
+ <div className='flex p-2 gap-2 items-center text-xs font-mono text-neutral-500 font-thin'>
138
+ <span>id:{thread.id.slice(0, 4)}</span>
139
+ <span>from:{thread.range?.from}</span>
140
+ <span>to:{thread.range?.to}</span>
141
+ <span>y:{thread.yPos}</span>
142
+ <span className='grow' />
143
+ {!thread.cursor && <Trash />}
144
+ </div>
145
+
146
+ {thread.messages.map((message, i) => (
147
+ // TODO(burdon): Fix default editor padding so content doesn't jump on creating message.
148
+ <div key={i} className='p-2' onClick={() => onSelect()}>
149
+ {getTextContent(message)}
150
+ </div>
151
+ ))}
152
+
153
+ <div ref={containerRef} className='flex' onClick={() => onSelect()}>
154
+ <TextEditor
155
+ ref={editorRef}
156
+ autoFocus={focus}
157
+ model={model}
158
+ placeholder={'Enter comment...'}
159
+ slots={{ root: { className: 'grow rounded-b' } }}
160
+ extensions={[
161
+ keymap.of([
162
+ {
163
+ key: 'Enter',
164
+ run: () => {
165
+ handleCreateMessage();
166
+ return true;
167
+ },
168
+ },
169
+ ]),
170
+ ]}
171
+ />
172
+ <Button variant='ghost' classNames='px-1' title='Resolve' onClick={onResolve}>
173
+ <Check />
174
+ </Button>
175
+ </div>
176
+ </div>
177
+ );
178
+ };
179
+
180
+ const Sidebar: FC<{
181
+ threads: CommentThread[];
182
+ selected?: string;
183
+ onSelect: (thread: string) => void;
184
+ onResolve: (thread: string) => void;
185
+ }> = ({ threads, selected, onSelect, onResolve }) => {
186
+ // Sort by y-position.
187
+ const sortedThreads = useMemo(() => {
188
+ const sorted = [...threads];
189
+ return sorted.sort(({ yPos: a = Infinity }, { yPos: b = Infinity }) => {
190
+ return a < b ? -1 : a > b ? 1 : 0;
191
+ });
192
+ }, [threads]);
193
+
194
+ return (
195
+ <DensityProvider density='fine'>
196
+ <div className='flex flex-col grow overflow-y-scroll py-4 gap-4'>
197
+ {sortedThreads.map((thread) => (
198
+ <Thread
199
+ key={thread.id}
200
+ thread={thread}
201
+ selected={thread.id === selected}
202
+ onSelect={() => onSelect(thread.id)}
203
+ onResolve={() => onResolve(thread.id)}
204
+ />
205
+ ))}
206
+ </div>
207
+ </DensityProvider>
208
+ );
209
+ };
210
+
211
+ //
212
+ // Story container.
213
+ //
214
+
215
+ type StoryProps = {
216
+ text?: string;
217
+ autoCreate?: boolean;
218
+ };
219
+
220
+ const Story = ({ text, autoCreate }: StoryProps) => {
221
+ const [item] = useState({ text: new TextObject(text) });
222
+ const [threads, setThreads] = useState<CommentThread[]>([]);
223
+ const [selected, setSelected] = useState<string>();
224
+
225
+ const comments = useMemo<Comment[]>(() => threads.map(({ id, cursor }) => ({ id, cursor })), [threads]);
226
+
227
+ // Filter by visibility.
228
+ const visibleThreads = useMemo(() => threads.filter((thread) => thread.yPos !== undefined), [threads]);
229
+
230
+ const handleCreateComment: CommentsOptions['onCreate'] = (cursor, location) => {
231
+ const id = PublicKey.random().toHex();
232
+ log.info('create', { id: id.slice(0, 4), cursor });
233
+ setThreads((threads) => [
234
+ ...threads,
235
+ {
236
+ id,
237
+ cursor,
238
+ yPos: location ? Math.floor(location.top) : undefined,
239
+ messages: autoCreate
240
+ ? faker.helpers.multiple(() => new TextObject(faker.lorem.sentence()), { count: { min: 1, max: 3 } })
241
+ : [],
242
+ },
243
+ ]);
244
+ setSelected(id); // TODO(burdon): Not required.
245
+ return id;
246
+ };
247
+
248
+ const handleDeleteComment: CommentsOptions['onDelete'] = (id) => {
249
+ log.info('delete', { id: id.slice(0, 4) });
250
+ setThreads((threads) =>
251
+ threads.map((thread) => {
252
+ if (thread.id === id) {
253
+ thread.cursor = undefined;
254
+ }
255
+
256
+ return thread;
257
+ }),
258
+ );
259
+
260
+ setSelected((selected) => (selected === id ? undefined : selected));
261
+ };
262
+
263
+ const handleUpdateComment: CommentsOptions['onUpdate'] = (id, cursor) => {
264
+ log.info('update', { id: id.slice(0, 4), cursor });
265
+ setThreads((threads) =>
266
+ threads.map((thread) => {
267
+ if (thread.id === id) {
268
+ thread.cursor = cursor;
269
+ }
270
+
271
+ return thread;
272
+ }),
273
+ );
274
+ };
275
+
276
+ const handleSelectComment: CommentsOptions['onSelect'] = ({ comments, selection: { current, closest } }) => {
277
+ log.info('select', { active: current?.slice(0, 4), closest: closest?.slice(0, 4) });
278
+ setThreads((threads) =>
279
+ threads.map((thread) => {
280
+ const comment = comments.find(({ comment }) => comment.id === thread.id);
281
+ if (comment) {
282
+ thread.yPos = comment.location ? Math.floor(comment.location.top) : undefined;
283
+ thread.range = { from: comment.range.from, to: comment.range.to };
284
+ }
285
+
286
+ return thread;
287
+ }),
288
+ );
289
+
290
+ setSelected(current ?? closest);
291
+ };
292
+
293
+ const handleSelectThread = (id: string) => {
294
+ setSelected(id);
295
+ };
296
+
297
+ const handleResolveThread = (id: string) => {
298
+ setThreads((threads) => [...threads.filter((thread) => thread.id !== id)]);
299
+ setSelected(undefined);
300
+ };
301
+
302
+ return (
303
+ <div className={mx(fixedInsetFlexLayout, 'bg-neutral-100')}>
304
+ <div className='flex justify-center h-full gap-8'>
305
+ <div className='flex flex-col h-full w-[600px]'>
306
+ <Editor
307
+ item={item}
308
+ selected={selected}
309
+ comments={comments}
310
+ onCreateComment={handleCreateComment}
311
+ onDeleteComment={handleDeleteComment}
312
+ onUpdateComment={handleUpdateComment}
313
+ onSelectComment={handleSelectComment}
314
+ />
315
+ </div>
316
+
317
+ <div className='flex flex-col h-full w-[300px]'>
318
+ <Sidebar
319
+ threads={visibleThreads}
320
+ selected={selected}
321
+ onSelect={handleSelectThread}
322
+ onResolve={handleResolveThread}
323
+ />
324
+ </div>
325
+ </div>
326
+ </div>
327
+ );
328
+ };
329
+
330
+ export default {
331
+ title: 'react-ui-editor/comments',
332
+ component: MarkdownEditor,
333
+ decorators: [withTheme],
334
+ render: (args: StoryProps) => <Story {...args} />,
335
+ };
336
+
337
+ const str = (...lines: string[]) => lines.join('\n');
338
+
339
+ const document = str(
340
+ '# Comments',
341
+ '',
342
+ str(...faker.helpers.multiple(() => [faker.lorem.paragraph({ min: 5, max: 10 }), ''], { count: 20 }).flat()),
343
+ '',
344
+ );
345
+
346
+ export const Default = {
347
+ args: {
348
+ text: document,
349
+ },
350
+ };
351
+
352
+ export const Testing = {
353
+ args: {
354
+ text: document,
355
+ autoCreate: true,
356
+ },
357
+ };
@@ -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
+ };