@dxos/react-ui-editor 0.3.10-main.5c4230b → 0.3.10-main.5e11230

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 (148) hide show
  1. package/dist/lib/browser/index.mjs +1644 -791
  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 +56 -0
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -0
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts +7 -3
  7. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  8. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +9 -4
  9. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  10. package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts +10 -0
  11. package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts.map +1 -0
  12. package/dist/types/src/components/TextEditor/extensions/{basic/bundle.d.ts → basic.d.ts} +3 -2
  13. package/dist/types/src/components/TextEditor/extensions/basic.d.ts.map +1 -0
  14. package/dist/types/src/components/TextEditor/extensions/blast.d.ts +25 -0
  15. package/dist/types/src/components/TextEditor/extensions/blast.d.ts.map +1 -0
  16. package/dist/types/src/components/TextEditor/extensions/comments.d.ts +18 -0
  17. package/dist/types/src/components/TextEditor/extensions/comments.d.ts.map +1 -0
  18. package/dist/types/src/components/TextEditor/extensions/demo.d.ts +11 -0
  19. package/dist/types/src/components/TextEditor/extensions/demo.d.ts.map +1 -0
  20. package/dist/types/src/components/TextEditor/extensions/{hyperlink/hyperlinkWidget.d.ts → experimental.d.ts} +2 -2
  21. package/dist/types/src/components/TextEditor/extensions/experimental.d.ts.map +1 -0
  22. package/dist/types/src/components/TextEditor/extensions/image.d.ts +4 -0
  23. package/dist/types/src/components/TextEditor/extensions/image.d.ts.map +1 -0
  24. package/dist/types/src/components/TextEditor/extensions/index.d.ts +11 -2
  25. package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -1
  26. package/dist/types/src/components/TextEditor/extensions/link.d.ts +11 -0
  27. package/dist/types/src/components/TextEditor/extensions/link.d.ts.map +1 -0
  28. package/dist/types/src/components/TextEditor/extensions/listener.d.ts +9 -0
  29. package/dist/types/src/components/TextEditor/extensions/listener.d.ts.map +1 -0
  30. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts +9 -1
  31. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +1 -1
  32. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts +37 -0
  33. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +1 -0
  34. package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts +1 -1
  35. package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts.map +1 -1
  36. package/dist/types/src/components/TextEditor/extensions/mention.d.ts +6 -0
  37. package/dist/types/src/components/TextEditor/extensions/mention.d.ts.map +1 -0
  38. package/dist/types/src/components/TextEditor/extensions/table.d.ts +8 -0
  39. package/dist/types/src/components/TextEditor/extensions/table.d.ts.map +1 -0
  40. package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts +3 -0
  41. package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts.map +1 -0
  42. package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts +12 -0
  43. package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts.map +1 -0
  44. package/dist/types/src/components/TextEditor/index.d.ts +2 -0
  45. package/dist/types/src/components/TextEditor/index.d.ts.map +1 -1
  46. package/dist/types/src/components/TextEditor/themes/default.d.ts +25 -0
  47. package/dist/types/src/components/TextEditor/themes/default.d.ts.map +1 -0
  48. package/dist/types/src/components/TextEditor/themes/index.d.ts +2 -0
  49. package/dist/types/src/components/TextEditor/themes/index.d.ts.map +1 -0
  50. package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/PatchSemaphore.d.ts +3 -0
  51. package/dist/types/src/hooks/automerge/PatchSemaphore.d.ts.map +1 -0
  52. package/dist/types/src/hooks/automerge/automerge.stories.d.ts +16 -0
  53. package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +1 -0
  54. package/dist/types/src/hooks/automerge/handle.d.ts.map +1 -0
  55. package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/index.d.ts +1 -1
  56. package/dist/types/src/hooks/automerge/index.d.ts.map +1 -0
  57. package/dist/types/src/hooks/automerge/plugin.d.ts.map +1 -0
  58. package/dist/types/src/hooks/automerge/translation/automerge-to-codemirror.d.ts +5 -0
  59. package/dist/types/src/hooks/automerge/translation/automerge-to-codemirror.d.ts.map +1 -0
  60. package/dist/types/src/hooks/automerge/translation/codemirror-to-automerge.d.ts +6 -0
  61. package/dist/types/src/hooks/automerge/translation/codemirror-to-automerge.d.ts.map +1 -0
  62. package/dist/types/src/hooks/automerge/translation/index.d.ts +3 -0
  63. package/dist/types/src/hooks/automerge/translation/index.d.ts.map +1 -0
  64. package/dist/types/src/hooks/index.d.ts +0 -1
  65. package/dist/types/src/hooks/index.d.ts.map +1 -1
  66. package/dist/types/src/hooks/useTextModel.d.ts +4 -4
  67. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  68. package/dist/types/src/{components/TextEditor → hooks/yjs}/yjs.stories.d.ts +2 -1
  69. package/dist/types/src/hooks/yjs/yjs.stories.d.ts.map +1 -0
  70. package/dist/types/src/styles/markdown.d.ts +14 -11
  71. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  72. package/dist/types/src/testing/replicator.d.ts.map +1 -1
  73. package/package.json +21 -32
  74. package/src/components/TextEditor/MarkdownEditor.stories.tsx +350 -0
  75. package/src/components/TextEditor/TextEditor.stories.tsx +46 -43
  76. package/src/components/TextEditor/TextEditor.tsx +71 -45
  77. package/src/components/TextEditor/extensions/autocomplete.ts +59 -0
  78. package/src/components/TextEditor/extensions/{basic/bundle.ts → basic.ts} +6 -3
  79. package/src/components/TextEditor/extensions/blast.ts +387 -0
  80. package/src/components/TextEditor/extensions/comments.ts +172 -0
  81. package/src/components/TextEditor/extensions/demo.ts +69 -0
  82. package/src/components/TextEditor/extensions/{hyperlink/hyperlinkWidget.tsx → experimental.tsx} +15 -20
  83. package/src/components/TextEditor/extensions/image.ts +67 -0
  84. package/src/components/TextEditor/extensions/index.ts +11 -2
  85. package/src/components/TextEditor/extensions/{hyperlink/hyperlinkDecoration.ts → link.ts} +75 -38
  86. package/src/components/TextEditor/extensions/{change.ts → listener.ts} +5 -4
  87. package/src/components/TextEditor/extensions/markdown/bundle.ts +56 -59
  88. package/src/components/TextEditor/extensions/markdown/highlight.ts +207 -0
  89. package/src/components/TextEditor/extensions/markdown/index.ts +1 -1
  90. package/src/components/TextEditor/extensions/mention.ts +31 -0
  91. package/src/components/TextEditor/extensions/table.ts +122 -0
  92. package/src/components/TextEditor/extensions/tasklist.ts +121 -0
  93. package/src/components/TextEditor/extensions/{hyperlink/hyperlinkTooltip.tsx → tooltip.tsx} +9 -9
  94. package/src/components/TextEditor/index.ts +3 -0
  95. package/src/components/TextEditor/themes/default.ts +274 -0
  96. package/src/components/TextEditor/themes/index.ts +5 -0
  97. package/src/{automerge/automerge-plugin → hooks/automerge}/PatchSemaphore.ts +12 -9
  98. package/src/{automerge → hooks/automerge}/automerge.stories.tsx +5 -3
  99. package/src/{automerge/automerge-plugin → hooks/automerge}/plugin.ts +1 -0
  100. package/src/{automerge/automerge-plugin/amToCodemirror.ts → hooks/automerge/translation/automerge-to-codemirror.ts} +10 -2
  101. package/src/{automerge/automerge-plugin/codeMirrorToAm.ts → hooks/automerge/translation/codemirror-to-automerge.ts} +7 -4
  102. package/src/hooks/automerge/translation/index.ts +6 -0
  103. package/src/hooks/index.ts +0 -1
  104. package/src/hooks/useTextModel.ts +52 -39
  105. package/src/{components/TextEditor → hooks/yjs}/yjs.stories.tsx +2 -1
  106. package/src/styles/markdown.ts +32 -24
  107. package/src/testing/replicator.ts +6 -6
  108. package/dist/types/src/automerge/automerge-plugin/PatchSemaphore.d.ts.map +0 -1
  109. package/dist/types/src/automerge/automerge-plugin/amToCodemirror.d.ts +0 -6
  110. package/dist/types/src/automerge/automerge-plugin/amToCodemirror.d.ts.map +0 -1
  111. package/dist/types/src/automerge/automerge-plugin/codeMirrorToAm.d.ts +0 -7
  112. package/dist/types/src/automerge/automerge-plugin/codeMirrorToAm.d.ts.map +0 -1
  113. package/dist/types/src/automerge/automerge-plugin/handle.d.ts.map +0 -1
  114. package/dist/types/src/automerge/automerge-plugin/index.d.ts.map +0 -1
  115. package/dist/types/src/automerge/automerge-plugin/plugin.d.ts.map +0 -1
  116. package/dist/types/src/automerge/automerge.stories.d.ts +0 -9
  117. package/dist/types/src/automerge/automerge.stories.d.ts.map +0 -1
  118. package/dist/types/src/components/TextEditor/extensions/basic/bundle.d.ts.map +0 -1
  119. package/dist/types/src/components/TextEditor/extensions/basic/index.d.ts +0 -3
  120. package/dist/types/src/components/TextEditor/extensions/basic/index.d.ts.map +0 -1
  121. package/dist/types/src/components/TextEditor/extensions/basic/theme.d.ts +0 -10
  122. package/dist/types/src/components/TextEditor/extensions/basic/theme.d.ts.map +0 -1
  123. package/dist/types/src/components/TextEditor/extensions/change.d.ts +0 -7
  124. package/dist/types/src/components/TextEditor/extensions/change.d.ts.map +0 -1
  125. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkDecoration.d.ts +0 -11
  126. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkDecoration.d.ts.map +0 -1
  127. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkTooltip.d.ts +0 -8
  128. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkTooltip.d.ts.map +0 -1
  129. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkWidget.d.ts.map +0 -1
  130. package/dist/types/src/components/TextEditor/extensions/hyperlink/index.d.ts +0 -4
  131. package/dist/types/src/components/TextEditor/extensions/hyperlink/index.d.ts.map +0 -1
  132. package/dist/types/src/components/TextEditor/extensions/markdown/tags.d.ts +0 -17
  133. package/dist/types/src/components/TextEditor/extensions/markdown/tags.d.ts.map +0 -1
  134. package/dist/types/src/components/TextEditor/extensions/markdown/theme.d.ts +0 -21
  135. package/dist/types/src/components/TextEditor/extensions/markdown/theme.d.ts.map +0 -1
  136. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
  137. package/dist/types/src/hooks/useCollaboration.d.ts +0 -9
  138. package/dist/types/src/hooks/useCollaboration.d.ts.map +0 -1
  139. package/src/components/TextEditor/extensions/basic/index.ts +0 -6
  140. package/src/components/TextEditor/extensions/basic/theme.ts +0 -49
  141. package/src/components/TextEditor/extensions/hyperlink/index.ts +0 -7
  142. package/src/components/TextEditor/extensions/markdown/tags.ts +0 -38
  143. package/src/components/TextEditor/extensions/markdown/theme.ts +0 -265
  144. package/src/hooks/useCollaboration.ts +0 -45
  145. package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/handle.d.ts +0 -0
  146. package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/plugin.d.ts +0 -0
  147. package/src/{automerge/automerge-plugin → hooks/automerge}/handle.ts +0 -0
  148. package/src/{automerge/automerge-plugin → hooks/automerge}/index.ts +1 -1
@@ -0,0 +1,350 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import '@dxosTheme';
6
+
7
+ import { EditorView } from '@codemirror/view';
8
+ import { faker } from '@faker-js/faker';
9
+ import { ArrowSquareOut } from '@phosphor-icons/react';
10
+ import defaultsDeep from 'lodash.defaultsdeep';
11
+ import React, { StrictMode, useRef, useState } from 'react';
12
+ import { createRoot } from 'react-dom/client';
13
+
14
+ import { TextObject } from '@dxos/echo-schema';
15
+ import { PublicKey } from '@dxos/keys';
16
+ import { fixedInsetFlexLayout, getSize, groupSurface, mx } from '@dxos/react-ui-theme';
17
+ import { withTheme } from '@dxos/storybook-utils';
18
+
19
+ import { MarkdownEditor, type TextEditorProps, type TextEditorRef } from './TextEditor';
20
+ import {
21
+ autocomplete,
22
+ listener,
23
+ link,
24
+ tasklist,
25
+ tooltip,
26
+ type TooltipOptions,
27
+ type LinkOptions,
28
+ comments,
29
+ table,
30
+ image,
31
+ mention,
32
+ blast,
33
+ demo,
34
+ defaultOptions,
35
+ } from './extensions';
36
+ import { useTextModel } from '../../hooks';
37
+
38
+ const str = (...lines: string[]) => lines.join('\n');
39
+
40
+ const num = () => faker.number.int({ min: 0, max: 9999 }).toLocaleString();
41
+
42
+ // prettier-ignore
43
+ const text = {
44
+ tasks: str(
45
+ '## Tasks',
46
+ '',
47
+ '- [x] decorator',
48
+ '- [ ] checkbox',
49
+ ' - [ ] state',
50
+ ' - [ ] indent',
51
+ ' - [x] style',
52
+ ),
53
+
54
+ list: str(
55
+ '## List',
56
+ '',
57
+ '- new york',
58
+ '- london',
59
+ '- tokyo',
60
+ ),
61
+
62
+ numbered: str(
63
+ '## Numbered',
64
+ '',
65
+ '1. one',
66
+ '2. two',
67
+ '3. three',
68
+ ''
69
+ ),
70
+
71
+ code: str(
72
+ '## Code',
73
+ '',
74
+ '```',
75
+ 'const x = 100;',
76
+ '```',
77
+ '',
78
+ '```tsx',
79
+ 'const Component = () => {',
80
+ ' const x = 100;',
81
+ ' return () => <div>Test</div>;',
82
+ '};',
83
+ '',
84
+ '```'
85
+ ),
86
+
87
+ links: str(
88
+ '## Links',
89
+ '',
90
+ 'This is a naked link https://dxos.org within a sentence.',
91
+ '',
92
+ 'Take a look at [DXOS](https://dxos.org) and how to [get started](https://docs.dxos.org/guide/getting-started.html).',
93
+ '',
94
+ ),
95
+
96
+ table: str(
97
+ '# Table',
98
+ '',
99
+ `| ${faker.lorem.word()} | ${faker.lorem.word()} | ${faker.lorem.word()} |`,
100
+ '|---|---|---|',
101
+ `| ${num()} | ${num()} | ${num()} |`,
102
+ `| ${num()} | ${num()} | ${num()} |`,
103
+ `| ${num()} | ${num()} | ${num()} |`,
104
+ '', // TODO(burdon): Possible GFM parsing bug if no newline?
105
+ ),
106
+
107
+ image: str('# Image', '', '![dxos](https://pbs.twimg.com/profile_banners/1268328127673044992/1684766689/1500x500)'),
108
+
109
+ headings: str(
110
+ ...[1, 2, 3, 4, 5, 6].map((level) => ['#'.repeat(level) + ` Heading ${level}`, faker.lorem.sentences(), '']).flat(),
111
+ ),
112
+
113
+ paragraphs: str(...faker.helpers.multiple(() => [faker.lorem.paragraph(), ''], { count: 3 }).flat()),
114
+ };
115
+
116
+ const document = str(
117
+ '# Markdown',
118
+ '',
119
+ '> This is a block quote.',
120
+ '',
121
+ 'This is all about https://dxos.org and related technologies.',
122
+ '',
123
+ 'This this is **bold**, ~~strikethrough~~, _italic_, and `f(INLINE)`.',
124
+ '',
125
+ '---',
126
+ text.links,
127
+ '---',
128
+ text.list,
129
+ '---',
130
+ text.tasks,
131
+ '---',
132
+ text.numbered,
133
+ '---',
134
+ text.code,
135
+ '---',
136
+ text.headings,
137
+ '---',
138
+ text.table,
139
+ '---',
140
+ text.image,
141
+ '',
142
+ );
143
+
144
+ const links = [
145
+ { label: 'DXOS', apply: '[DXOS](https://dxos.org)' },
146
+ { label: 'GitHub', apply: '[DXOS GitHub](https://github.com/dxos)' },
147
+ { label: 'Automerge', apply: '[Automerge](https://automerge.org/)' },
148
+ { label: 'IPFS', apply: '[Protocol Labs](https://docs.ipfs.tech)' },
149
+ { label: 'StackEdit', apply: '[StackEdit](https://stackedit.io/app)' },
150
+ ];
151
+
152
+ const hover =
153
+ 'rounded-sm text-base text-primary-600 hover:text-primary-500 dark:text-primary-300 hover:dark:text-primary-200';
154
+
155
+ const onHover: TooltipOptions['onHover'] = (el, url) => {
156
+ const web = new URL(url);
157
+ createRoot(el).render(
158
+ <StrictMode>
159
+ <a href={url} target='_blank' rel='noreferrer' className={hover}>
160
+ {web.origin}
161
+ <ArrowSquareOut weight='bold' className={mx(getSize(4), 'inline-block leading-none mis-1')} />
162
+ </a>
163
+ </StrictMode>,
164
+ );
165
+ };
166
+
167
+ const onRender: LinkOptions['onRender'] = (el, url) => {
168
+ createRoot(el).render(
169
+ <StrictMode>
170
+ <a href={url} target='_blank' rel='noreferrer' className={hover}>
171
+ <ArrowSquareOut weight='bold' className={mx(getSize(4), 'inline-block leading-none mis-1 mb-1')} />
172
+ </a>
173
+ </StrictMode>,
174
+ );
175
+ };
176
+
177
+ const Story = ({
178
+ text,
179
+ automerge,
180
+ ...props
181
+ }: { text?: string; automerge?: boolean } & Pick<TextEditorProps, 'readonly' | 'extensions' | 'slots'>) => {
182
+ const ref = useRef<TextEditorRef>(null);
183
+ const [item] = useState({ text: new TextObject(text, undefined, undefined, { useAutomergeBackend: automerge }) });
184
+ const model = useTextModel({ text: item.text });
185
+ if (!model) {
186
+ return null;
187
+ }
188
+
189
+ return (
190
+ <div className={mx(fixedInsetFlexLayout, groupSurface)}>
191
+ <div className='flex justify-center overflow-y-scroll'>
192
+ <div className='flex flex-col w-[800px] py-16'>
193
+ <MarkdownEditor ref={ref} model={model} {...props} />
194
+ <div className='flex shrink-0 h-[300px]'></div>
195
+ </div>
196
+ </div>
197
+ </div>
198
+ );
199
+ };
200
+
201
+ export default {
202
+ title: 'react-ui-editor/MarkdownEditor',
203
+ component: MarkdownEditor,
204
+ decorators: [withTheme],
205
+ render: Story,
206
+ };
207
+
208
+ const extensions = [
209
+ link({ onRender }),
210
+ tooltip({ onHover }),
211
+ tasklist(),
212
+ table(),
213
+ image(),
214
+ autocomplete({
215
+ onSearch: (text) => links.filter(({ label }) => label.toLowerCase().includes(text.toLowerCase())),
216
+ }),
217
+ ];
218
+
219
+ export const Default = {
220
+ render: () => <Story text={document} extensions={extensions} />,
221
+ };
222
+
223
+ export const Readonly = {
224
+ render: () => <Story text={document} extensions={extensions} readonly />,
225
+ };
226
+
227
+ export const Simple = {
228
+ render: () => <Story text={document} />,
229
+ };
230
+
231
+ export const Tooltips = {
232
+ render: () => <Story text={str(text.links, '')} extensions={[tooltip({ onHover })]} />,
233
+ };
234
+
235
+ export const Links = {
236
+ render: () => <Story text={str(text.links, '')} extensions={[link({ onRender })]} />,
237
+ };
238
+
239
+ export const Table = {
240
+ render: () => <Story text={str(text.table, '')} extensions={[table()]} />,
241
+ };
242
+
243
+ export const Image = {
244
+ render: () => <Story text={str(text.image, '', '')} readonly extensions={[image()]} />,
245
+ };
246
+
247
+ export const TaskList = {
248
+ render: () => (
249
+ <Story
250
+ text={str(text.tasks, '', text.list)}
251
+ extensions={[
252
+ tasklist(),
253
+ listener({
254
+ onChange: (text) => {
255
+ console.log(text);
256
+ },
257
+ }),
258
+ ]}
259
+ />
260
+ ),
261
+ };
262
+
263
+ export const Autocomplete = {
264
+ render: () => (
265
+ <Story
266
+ text={str('# Autocomplete', '', '', '', '', '', '')}
267
+ extensions={[
268
+ link({ onRender }),
269
+ autocomplete({
270
+ onSearch: (text) => links.filter(({ label }) => label.toLowerCase().includes(text.toLowerCase())),
271
+ }),
272
+ ]}
273
+ />
274
+ ),
275
+ };
276
+
277
+ const names = ['adam', 'alice', 'alison', 'bob', 'carol', 'charlie', 'sayuri', 'shoko'];
278
+
279
+ export const Mention = {
280
+ render: () => (
281
+ <Story
282
+ text={str('# Mention', '', '', '', '', '', '')}
283
+ extensions={[
284
+ mention({
285
+ onSearch: (text) => names.filter((name) => name.toLowerCase().startsWith(text.toLowerCase())),
286
+ }),
287
+ ]}
288
+ />
289
+ ),
290
+ };
291
+
292
+ const mark = () => `[^${PublicKey.random().toHex()}]`;
293
+ export const Comments = {
294
+ render: () => (
295
+ <Story
296
+ text={str(mark(), '', text.paragraphs, mark(), '', mark(), '')}
297
+ extensions={[
298
+ comments({
299
+ onCreate: () => PublicKey.random().toHex(),
300
+ onUpdate: (info) => {
301
+ console.log('update', info);
302
+ },
303
+ }),
304
+ ]}
305
+ />
306
+ ),
307
+ };
308
+
309
+ export const Diagnostics = {
310
+ render: () => (
311
+ <Story
312
+ text={document}
313
+ extensions={[
314
+ // Cursor moved.
315
+ EditorView.updateListener.of((update) => {
316
+ console.log('update', update.view.state.selection.main.head);
317
+ }),
318
+ ]}
319
+ />
320
+ ),
321
+ };
322
+
323
+ export const Demo = {
324
+ render: () => <Story text={text.paragraphs} extensions={[demo()]} />,
325
+ };
326
+
327
+ export const Blast = {
328
+ render: () => (
329
+ <Story
330
+ text={str(text.paragraphs, text.code, text.paragraphs)}
331
+ extensions={[
332
+ demo({
333
+ items: localStorage.getItem('dxos.composer.extension.demo')?.split(','),
334
+ }),
335
+ blast(
336
+ defaultsDeep(
337
+ {
338
+ effect: 2,
339
+ particleGravity: 0.2,
340
+ particleShrinkRate: 0.995,
341
+ color: () => [faker.number.int({ min: 100, max: 200 }), 0, 0],
342
+ // color: () => [faker.number.int(256), faker.number.int(256), faker.number.int(256)],
343
+ },
344
+ defaultOptions,
345
+ ),
346
+ ),
347
+ ]}
348
+ />
349
+ ),
350
+ };
@@ -3,63 +3,46 @@
3
3
  //
4
4
 
5
5
  import '@dxosTheme';
6
- import { markdown } from '@codemirror/lang-markdown';
7
- import { ArrowCircleUp } from '@phosphor-icons/react';
8
- import React, { StrictMode, useState } from 'react';
9
- import { createRoot } from 'react-dom/client';
6
+
7
+ import defaultsDeep from 'lodash.defaultsdeep';
8
+ import React, { useRef, useState } from 'react';
10
9
 
11
10
  import { TextObject } from '@dxos/echo-schema';
12
- import { fixedInsetFlexLayout, getSize, groupSurface, inputSurface, mx } from '@dxos/react-ui-theme';
11
+ import { fixedInsetFlexLayout, groupSurface, mx } from '@dxos/react-ui-theme';
13
12
  import { withTheme } from '@dxos/storybook-utils';
14
13
 
15
- import { TextEditor } from './TextEditor';
16
- import { createHyperlinkTooltip, hyperlinkDecoration } from './extensions';
14
+ import { defaultSlots, TextEditor, type TextEditorProps, type TextEditorRef, type TextEditorSlots } from './TextEditor';
15
+ import { textTheme } from './themes';
17
16
  import { useTextModel } from '../../hooks';
18
17
 
19
- const text = [
20
- '',
21
- '',
22
- 'This is all about [DXOS](https://dxos.org); read more [here](https://docs.dxos.org/guide/getting-started.html).',
18
+ const initialText = [
19
+ '# TextEditor',
23
20
  '',
21
+ 'This is the basic plain text editor within minimal formatting.',
22
+ 'You can add custom styles and create custom extensions.',
23
+ 'Or use the MarkdownEditor to edit documents.',
24
24
  '',
25
+ 'https://dxos.org',
25
26
  ].join('\n');
26
27
 
27
- const hyperLinkTooltip = () =>
28
- createHyperlinkTooltip((el, url) => {
29
- const web = new URL(url);
30
- createRoot(el).render(
31
- <StrictMode>
32
- <div className='flex gap-1 items-center'>
33
- <ArrowCircleUp className={mx(getSize(6), 'text-blue-500')} />
34
- <p className='pr-1'>{web.origin}</p>
35
- </div>
36
- </StrictMode>,
37
- );
38
- });
39
-
40
- const Story = ({ automerge }: { automerge?: boolean }) => {
41
- const [item] = useState({ text: new TextObject(text, undefined, undefined, { useAutomergeBackend: !!automerge }) });
28
+ const Story = ({
29
+ text,
30
+ automerge,
31
+ ...props
32
+ }: { text?: string; automerge?: boolean } & Pick<TextEditorProps, 'extensions' | 'slots'>) => {
33
+ const ref = useRef<TextEditorRef>(null);
34
+ const [item] = useState({ text: new TextObject(text, undefined, undefined, { useAutomergeBackend: automerge }) });
42
35
  const model = useTextModel({ text: item.text });
43
36
  if (!model) {
44
- return <></>;
37
+ return null;
45
38
  }
46
39
 
47
40
  return (
48
41
  <div className={mx(fixedInsetFlexLayout, groupSurface)}>
49
- <div className='flex justify-center p-8'>
50
- <div className='w-[800px]'>
51
- <TextEditor
52
- model={model}
53
- extensions={[
54
- markdown(),
55
- hyperlinkDecoration({ link: false }),
56
- hyperLinkTooltip(),
57
- // EditorView.domEventHandlers({
58
- // mousedown: (e, view) => {},
59
- // }),
60
- ]}
61
- slots={{ root: { className: mx(inputSurface, 'p-2') } }}
62
- />
42
+ <div className='flex justify-center overflow-y-scroll'>
43
+ <div className='flex flex-col w-[800px] py-16'>
44
+ <TextEditor ref={ref} model={model} {...props} />
45
+ <div className='flex shrink-0 h-[300px]'></div>
63
46
  </div>
64
47
  </div>
65
48
  </div>
@@ -67,13 +50,33 @@ const Story = ({ automerge }: { automerge?: boolean }) => {
67
50
  };
68
51
 
69
52
  export default {
53
+ title: 'react-ui-editor/TextEditor',
70
54
  component: TextEditor,
71
55
  decorators: [withTheme],
72
56
  render: Story,
73
57
  };
74
58
 
75
- export const Default = {};
59
+ export const Default = {
60
+ render: () => (
61
+ <Story
62
+ text={initialText}
63
+ slots={defaultsDeep(
64
+ { editor: { theme: textTheme, placeholder: 'Enter text...' } } satisfies TextEditorSlots,
65
+ defaultSlots,
66
+ )}
67
+ />
68
+ ),
69
+ };
76
70
 
77
71
  export const Automerge = {
78
- render: () => <Story automerge />,
72
+ render: () => (
73
+ <Story
74
+ text={initialText}
75
+ slots={defaultsDeep(
76
+ { editor: { theme: textTheme, placeholder: 'Enter text...' } } satisfies TextEditorSlots,
77
+ defaultSlots,
78
+ )}
79
+ automerge
80
+ />
81
+ ),
79
82
  };
@@ -7,23 +7,24 @@ import { EditorView } from '@codemirror/view';
7
7
  import { useFocusableGroup } from '@fluentui/react-tabster';
8
8
  import { vim } from '@replit/codemirror-vim';
9
9
  import defaultsDeep from 'lodash.defaultsdeep';
10
- import get from 'lodash.get';
11
10
  import React, {
11
+ type ComponentProps,
12
12
  type KeyboardEvent,
13
13
  forwardRef,
14
14
  useCallback,
15
15
  useEffect,
16
16
  useImperativeHandle,
17
17
  useState,
18
- type ComponentProps,
19
18
  } from 'react';
20
19
 
21
- import { isDocAccessor } from '@dxos/echo-schema';
20
+ import { generateName } from '@dxos/display-name';
22
21
  import { useThemeContext } from '@dxos/react-ui';
22
+ import { getColorForValue, inputSurface, mx } from '@dxos/react-ui-theme';
23
23
 
24
- import { basicBundle, basicTheme, markdownBundle, markdownTheme } from './extensions';
25
- import { type EditorModel, useCollaboration } from '../../hooks';
26
- import type { ThemeStyles } from '../../styles';
24
+ import { basicBundle, markdownBundle } from './extensions';
25
+ import { defaultTheme, markdownTheme, textTheme } from './themes';
26
+ import { type EditorModel } from '../../hooks';
27
+ import { type ThemeStyles } from '../../styles';
27
28
 
28
29
  export const EditorModes = ['default', 'vim'] as const;
29
30
  export type EditorMode = (typeof EditorModes)[number];
@@ -37,7 +38,7 @@ export type CursorInfo = {
37
38
  };
38
39
 
39
40
  export type TextEditorRef = {
40
- editor: HTMLDivElement | null;
41
+ root: HTMLDivElement | null;
41
42
  state?: EditorState;
42
43
  view?: EditorView;
43
44
  };
@@ -55,9 +56,10 @@ export type TextEditorSlots = {
55
56
 
56
57
  export type TextEditorProps = {
57
58
  model: EditorModel;
59
+ readonly?: boolean;
60
+ editorMode?: EditorMode;
58
61
  extensions?: Extension[];
59
62
  slots?: TextEditorSlots;
60
- editorMode?: EditorMode;
61
63
  };
62
64
 
63
65
  /**
@@ -65,66 +67,68 @@ export type TextEditorProps = {
65
67
  * NOTE: Rather than adding properties, try to create extensions that can be reused.
66
68
  */
67
69
  export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
68
- ({ model, extensions = [], slots, editorMode }, forwardedRef) => {
70
+ ({ model, readonly, editorMode, extensions = [], slots = defaultSlots }, forwardedRef) => {
69
71
  const { themeMode } = useThemeContext();
70
72
  const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
71
73
 
72
- const [parent, setParent] = useState<HTMLDivElement | null>(null);
74
+ const [root, setRoot] = useState<HTMLDivElement | null>(null);
73
75
  const [state, setState] = useState<EditorState>();
74
76
  const [view, setView] = useState<EditorView>();
75
- useImperativeHandle(
76
- forwardedRef,
77
- () => ({
78
- editor: parent,
79
- state,
80
- view,
81
- }),
82
- [view, state, parent],
83
- );
77
+ useImperativeHandle(forwardedRef, () => ({ root, state, view }), [view, state, root]);
84
78
 
85
- // TODO(burdon): Pass in extension?
86
- const collaboration = useCollaboration(model, themeMode);
79
+ // TODO(burdon): Factor out?
80
+ const { awareness, peer } = model;
81
+ useEffect(() => {
82
+ if (awareness && peer) {
83
+ awareness.setLocalStateField('user', {
84
+ name: peer.name ?? generateName(peer.id),
85
+ color: getColorForValue({ value: peer.id, type: 'color' }),
86
+ colorLight: getColorForValue({ value: peer.id, themeMode, type: 'highlight' }),
87
+ });
88
+ }
89
+ }, [awareness, peer, themeMode]);
87
90
 
88
91
  useEffect(() => {
89
- if (!parent) {
92
+ if (!root) {
90
93
  return;
91
94
  }
92
95
 
93
- const initialText = isDocAccessor(model.content)
94
- ? get(model.content.handle.docSync(), model.content.path)
95
- : model.content?.toString();
96
-
97
96
  const state = EditorState.create({
98
- doc: initialText,
97
+ doc: model.text(),
99
98
  extensions: [
99
+ readonly && EditorState.readOnly.of(readonly),
100
+
100
101
  // TODO(burdon): Factor out VIM mode?
101
102
  editorMode === 'vim' && vim(),
102
103
 
103
- // Replication.
104
- collaboration,
105
-
106
104
  // Theme.
107
- slots?.editor?.theme && EditorView.theme(slots?.editor?.theme),
105
+ // TODO(burdon): Make optional.
106
+ EditorView.baseTheme(defaultTheme),
107
+ EditorView.theme(slots?.editor?.theme ?? {}),
108
+ // TODO(burdon): themeMode doesn't change in storybooks.
109
+ EditorView.darkTheme.of(themeMode === 'dark'),
110
+
111
+ // Storage and replication.
112
+ model.extension,
108
113
 
109
114
  // Custom.
110
115
  ...extensions,
111
116
  ].filter(Boolean) as Extension[],
112
117
  });
113
118
 
114
- setState(state);
115
-
116
119
  // NOTE: This repaints the editor.
117
120
  // If the new state is derived from the old state, it will likely not be visible other than the cursor resetting.
118
121
  // Ideally this should not be hit except when changing between text objects.
119
122
  view?.destroy();
120
- setView(new EditorView({ state, parent }));
123
+ setView(new EditorView({ state, parent: root }));
124
+ setState(state);
121
125
 
122
126
  return () => {
123
127
  view?.destroy();
124
128
  setView(undefined);
125
129
  setState(undefined);
126
130
  };
127
- }, [parent, model.content, themeMode, editorMode]);
131
+ }, [root, model, themeMode, readonly, editorMode]);
128
132
 
129
133
  const handleKeyUp = useCallback(
130
134
  (event: KeyboardEvent) => {
@@ -136,7 +140,7 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
136
140
  }
137
141
 
138
142
  case 'Escape': {
139
- editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey) && parent?.focus();
143
+ editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey) && root?.focus();
140
144
  break;
141
145
  }
142
146
  }
@@ -147,7 +151,7 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
147
151
  return (
148
152
  <div
149
153
  key={model.id}
150
- ref={setParent}
154
+ ref={setRoot}
151
155
  tabIndex={0}
152
156
  {...slots?.root}
153
157
  {...(editorMode !== 'vim' && tabsterDOMAttribute)}
@@ -158,14 +162,15 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
158
162
  );
159
163
 
160
164
  export const TextEditor = forwardRef<TextEditorRef, TextEditorProps>(
161
- ({ extensions: _extensions, slots, ...props }, forwardedRef) => {
165
+ ({ readonly, extensions = [], slots: _slots, ...props }, forwardedRef) => {
162
166
  const { themeMode } = useThemeContext();
163
- const extensions = [...(_extensions ?? []), basicBundle({ themeMode, placeholder: slots?.editor?.placeholder })];
167
+ const slots = defaultsDeep({}, _slots, defaultTextSlots);
164
168
  return (
165
169
  <BaseTextEditor
166
170
  ref={forwardedRef}
167
- extensions={extensions}
168
- slots={defaultsDeep({}, slots, { editor: { theme: basicTheme } })}
171
+ readonly={readonly}
172
+ extensions={[basicBundle({ readonly, themeMode, placeholder: slots?.editor?.placeholder }), ...extensions]}
173
+ slots={slots}
169
174
  {...props}
170
175
  />
171
176
  );
@@ -173,16 +178,37 @@ export const TextEditor = forwardRef<TextEditorRef, TextEditorProps>(
173
178
  );
174
179
 
175
180
  export const MarkdownEditor = forwardRef<TextEditorRef, TextEditorProps>(
176
- ({ extensions: _extensions, slots, ...props }, forwardedRef) => {
181
+ ({ readonly, extensions = [], slots: _slots, ...props }, forwardedRef) => {
177
182
  const { themeMode } = useThemeContext();
178
- const extensions = [...(_extensions ?? []), markdownBundle({ themeMode })];
183
+ const slots = defaultsDeep({}, _slots, defaultMarkdownSlots);
179
184
  return (
180
185
  <BaseTextEditor
181
186
  ref={forwardedRef}
182
- extensions={extensions}
183
- slots={defaultsDeep({}, slots, { editor: { theme: markdownTheme } })}
187
+ readonly={readonly}
188
+ extensions={[markdownBundle({ readonly, themeMode, placeholder: slots?.editor?.placeholder }), ...extensions]}
189
+ slots={slots}
184
190
  {...props}
185
191
  />
186
192
  );
187
193
  },
188
194
  );
195
+
196
+ export const defaultSlots: TextEditorSlots = {
197
+ root: {
198
+ className: mx('p-2', inputSurface),
199
+ },
200
+ };
201
+
202
+ export const defaultTextSlots: TextEditorSlots = {
203
+ ...defaultSlots,
204
+ editor: {
205
+ theme: textTheme,
206
+ },
207
+ };
208
+
209
+ export const defaultMarkdownSlots: TextEditorSlots = {
210
+ ...defaultSlots,
211
+ editor: {
212
+ theme: markdownTheme,
213
+ },
214
+ };