@dxos/react-ui-editor 0.6.7 → 0.6.8-main.3be982f

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 (109) hide show
  1. package/dist/lib/browser/index.mjs +1019 -796
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/{hooks/InputMode.stories.d.ts → InputMode.stories.d.ts} +6 -4
  5. package/dist/types/src/InputMode.stories.d.ts.map +1 -0
  6. package/dist/types/src/{hooks/TextEditor.stories.d.ts → TextEditor.stories.d.ts} +43 -26
  7. package/dist/types/src/TextEditor.stories.d.ts.map +1 -0
  8. package/dist/types/src/components/Toolbar/Toolbar.d.ts +9 -9
  9. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
  10. package/dist/types/src/defaults.d.ts +10 -0
  11. package/dist/types/src/defaults.d.ts.map +1 -0
  12. package/dist/types/src/extensions/autocomplete.d.ts +2 -2
  13. package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
  14. package/dist/types/src/extensions/automerge/automerge.stories.d.ts +6 -4
  15. package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
  16. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
  17. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
  18. package/dist/types/src/extensions/blast.d.ts.map +1 -1
  19. package/dist/types/src/extensions/command/state.d.ts +1 -1
  20. package/dist/types/src/extensions/command/state.d.ts.map +1 -1
  21. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  22. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  23. package/dist/types/src/extensions/folding.d.ts +7 -0
  24. package/dist/types/src/extensions/folding.d.ts.map +1 -0
  25. package/dist/types/src/extensions/index.d.ts +1 -0
  26. package/dist/types/src/extensions/index.d.ts.map +1 -1
  27. package/dist/types/src/extensions/markdown/decorate.d.ts +5 -1
  28. package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
  29. package/dist/types/src/extensions/markdown/formatting.d.ts +9 -9
  30. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
  31. package/dist/types/src/extensions/markdown/image.d.ts +1 -1
  32. package/dist/types/src/extensions/markdown/image.d.ts.map +1 -1
  33. package/dist/types/src/extensions/markdown/link-paste.d.ts +6 -0
  34. package/dist/types/src/extensions/markdown/link-paste.d.ts.map +1 -0
  35. package/dist/types/src/extensions/markdown/link-paste.test.d.ts +2 -0
  36. package/dist/types/src/extensions/markdown/link-paste.test.d.ts.map +1 -0
  37. package/dist/types/src/extensions/markdown/parser.test.d.ts +2 -0
  38. package/dist/types/src/extensions/markdown/parser.test.d.ts.map +1 -0
  39. package/dist/types/src/extensions/state.d.ts.map +1 -1
  40. package/dist/types/src/extensions/util/error.d.ts +2 -0
  41. package/dist/types/src/extensions/util/error.d.ts.map +1 -0
  42. package/dist/types/src/extensions/util/index.d.ts +3 -1
  43. package/dist/types/src/extensions/util/index.d.ts.map +1 -1
  44. package/dist/types/src/extensions/util/overlap.d.ts.map +1 -1
  45. package/dist/types/src/extensions/util/react.d.ts +3 -0
  46. package/dist/types/src/extensions/util/react.d.ts.map +1 -0
  47. package/dist/types/src/hooks/useActionHandler.d.ts +1 -1
  48. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  49. package/dist/types/src/index.d.ts +1 -2
  50. package/dist/types/src/index.d.ts.map +1 -1
  51. package/dist/types/src/styles/index.d.ts +1 -1
  52. package/dist/types/src/styles/index.d.ts.map +1 -1
  53. package/dist/types/src/styles/markdown.d.ts +0 -1
  54. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  55. package/dist/types/src/styles/theme.d.ts +36 -0
  56. package/dist/types/src/styles/theme.d.ts.map +1 -0
  57. package/dist/types/src/styles/tokens.d.ts.map +1 -1
  58. package/dist/types/src/translations.d.ts +2 -0
  59. package/dist/types/src/translations.d.ts.map +1 -1
  60. package/dist/types/src/util.d.ts.map +1 -1
  61. package/package.json +26 -29
  62. package/src/{hooks/InputMode.stories.tsx → InputMode.stories.tsx} +6 -11
  63. package/src/{hooks/TextEditor.stories.tsx → TextEditor.stories.tsx} +139 -92
  64. package/src/components/Toolbar/Toolbar.tsx +17 -9
  65. package/src/defaults.ts +28 -0
  66. package/src/extensions/autocomplete.ts +24 -18
  67. package/src/extensions/automerge/automerge.stories.tsx +4 -6
  68. package/src/extensions/comments.ts +4 -0
  69. package/src/extensions/factories.ts +3 -2
  70. package/src/extensions/folding.tsx +34 -0
  71. package/src/extensions/index.ts +1 -0
  72. package/src/extensions/markdown/bundle.ts +1 -1
  73. package/src/extensions/markdown/decorate.ts +359 -129
  74. package/src/extensions/markdown/formatting.ts +10 -12
  75. package/src/extensions/markdown/image.ts +3 -1
  76. package/src/extensions/markdown/link-paste.test.ts +28 -0
  77. package/src/extensions/markdown/link-paste.ts +104 -0
  78. package/src/extensions/markdown/parser.test.ts +47 -0
  79. package/src/extensions/markdown/table.ts +21 -24
  80. package/src/extensions/util/error.ts +15 -0
  81. package/src/extensions/util/index.ts +3 -1
  82. package/src/extensions/util/overlap.ts +1 -0
  83. package/src/extensions/util/react.tsx +15 -0
  84. package/src/hooks/useTextEditor.ts +1 -1
  85. package/src/index.ts +2 -2
  86. package/src/styles/index.ts +1 -1
  87. package/src/styles/markdown.ts +4 -3
  88. package/src/{themes/default.ts → styles/theme.ts} +51 -43
  89. package/src/styles/tokens.ts +0 -1
  90. package/src/translations.ts +2 -0
  91. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +0 -57
  92. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +0 -1
  93. package/dist/types/src/extensions/markdown/linkPaste.d.ts +0 -16
  94. package/dist/types/src/extensions/markdown/linkPaste.d.ts.map +0 -1
  95. package/dist/types/src/extensions/markdown/linkPaste.test.d.ts +0 -2
  96. package/dist/types/src/extensions/markdown/linkPaste.test.d.ts.map +0 -1
  97. package/dist/types/src/hooks/InputMode.stories.d.ts.map +0 -1
  98. package/dist/types/src/hooks/TextEditor.stories.d.ts.map +0 -1
  99. package/dist/types/src/styles/layout.d.ts +0 -4
  100. package/dist/types/src/styles/layout.d.ts.map +0 -1
  101. package/dist/types/src/themes/default.d.ts +0 -14
  102. package/dist/types/src/themes/default.d.ts.map +0 -1
  103. package/dist/types/src/themes/index.d.ts +0 -2
  104. package/dist/types/src/themes/index.d.ts.map +0 -1
  105. package/src/components/Toolbar/Toolbar.stories.tsx +0 -119
  106. package/src/extensions/markdown/linkPaste.test.ts +0 -45
  107. package/src/extensions/markdown/linkPaste.ts +0 -113
  108. package/src/styles/layout.ts +0 -9
  109. package/src/themes/index.ts +0 -5
@@ -3,32 +3,31 @@
3
3
  //
4
4
 
5
5
  import '@dxosTheme';
6
+
6
7
  import { markdown } from '@codemirror/lang-markdown';
7
8
  import { ArrowSquareOut, X } from '@phosphor-icons/react';
8
9
  import { effect, useSignal } from '@preact/signals-react';
9
10
  import defaultsDeep from 'lodash.defaultsdeep';
10
- import React, { type FC, type KeyboardEvent, StrictMode, useMemo, useState } from 'react';
11
+ import React, { type FC, type KeyboardEvent, StrictMode, useState } from 'react';
11
12
  import { createRoot } from 'react-dom/client';
12
13
 
13
- import { TextType } from '@braneframe/types';
14
- import { create } from '@dxos/echo-schema';
14
+ import { create, Expando } from '@dxos/echo-schema';
15
15
  import { keySymbols, parseShortcut } from '@dxos/keyboard';
16
16
  import { PublicKey } from '@dxos/keys';
17
17
  import { log } from '@dxos/log';
18
18
  import { faker } from '@dxos/random';
19
19
  import { createDocAccessor, createEchoObject } from '@dxos/react-client/echo';
20
20
  import { Button, DensityProvider, Input, ThemeProvider, useThemeContext } from '@dxos/react-ui';
21
- import { baseSurface, defaultTx, getSize, mx, textBlockWidth } from '@dxos/react-ui-theme';
22
- import { withTheme } from '@dxos/storybook-utils';
21
+ import { baseSurface, defaultTx, mx, getSize } from '@dxos/react-ui-theme';
22
+ import { withFullscreen, withTheme } from '@dxos/storybook-utils';
23
23
 
24
- import { useTextEditor, type UseTextEditorProps } from './useTextEditor';
24
+ import { editorContent } from './defaults';
25
25
  import {
26
26
  InputModeExtensions,
27
27
  annotations,
28
28
  autocomplete,
29
29
  blast,
30
30
  command,
31
- // commentBlock,
32
31
  comments,
33
32
  createBasicExtensions,
34
33
  createDataExtensions,
@@ -38,6 +37,7 @@ import {
38
37
  decorateMarkdown,
39
38
  defaultOptions,
40
39
  dropFile,
40
+ folding,
41
41
  formattingKeymap,
42
42
  image,
43
43
  linkTooltip,
@@ -51,8 +51,9 @@ import {
51
51
  type Comment,
52
52
  type CommentsOptions,
53
53
  type SelectionState,
54
- } from '../extensions';
55
- import translations from '../translations';
54
+ } from './extensions';
55
+ import { useTextEditor, type UseTextEditorProps } from './hooks';
56
+ import translations from './translations';
56
57
 
57
58
  faker.seed(101);
58
59
 
@@ -62,10 +63,10 @@ const num = () => faker.number.int({ min: 0, max: 9999 }).toLocaleString();
62
63
 
63
64
  const img = '![dxos](https://pbs.twimg.com/profile_banners/1268328127673044992/1684766689/1500x500)';
64
65
 
65
- const text = {
66
+ const content = {
66
67
  tasks: str(
67
68
  //
68
- '## Tasks',
69
+ '### Task List',
69
70
  '',
70
71
  `- [x] ${faker.lorem.sentences()}`,
71
72
  `- [ ] ${faker.lorem.sentences()}`,
@@ -75,28 +76,38 @@ const text = {
75
76
  '',
76
77
  ),
77
78
 
78
- list: str(
79
+ bullets: str(
79
80
  //
80
- '## List',
81
+ '### BulletList',
81
82
  '',
82
83
  `- ${faker.lorem.sentences()}`,
83
84
  `- ${faker.lorem.sentences()}`,
85
+ ` - ${faker.lorem.sentences()}`,
86
+ ` - ${faker.lorem.sentences()}`,
84
87
  `- ${faker.lorem.sentences()}`,
85
88
  '',
86
89
  ),
87
90
 
88
91
  numbered: str(
89
92
  //
90
- '## Numbered',
93
+ '### OrderedList (part 1)',
94
+ '',
95
+ `1. ${faker.lorem.sentences()}`,
96
+ `1. ${faker.lorem.sentences()}`,
97
+ `1. ${faker.lorem.sentences()}`,
98
+ ` 1. ${faker.lorem.sentences()}`,
99
+ ` 1. ${faker.lorem.sentences()}`,
100
+ ` 1. ${faker.lorem.sentences()}`,
101
+ `1. ${faker.lorem.sentences()}`,
102
+ '',
103
+ '### OrderedList (part 2)',
91
104
  '',
92
105
  `1. ${faker.lorem.sentences()}`,
93
- `2. ${faker.lorem.sentences()}`,
94
- `3. ${faker.lorem.sentences()}`,
95
106
  '',
96
107
  ),
97
108
 
98
109
  code: str(
99
- '## Code',
110
+ '### Code',
100
111
  '',
101
112
  '```',
102
113
  '$ ls -las',
@@ -115,16 +126,18 @@ const text = {
115
126
  comment: str('<!--', 'A comment', '-->', '', 'No comment.', 'Partial comment. <!-- comment. -->'),
116
127
 
117
128
  links: str(
118
- '## Links',
129
+ '### Links',
119
130
  '',
120
131
  'This is a naked link https://dxos.org within a sentence.',
121
132
  '',
122
133
  'Take a look at [DXOS](https://dxos.org) and how to [get started](https://docs.dxos.org/guide/getting-started.html).',
123
134
  '',
135
+ 'This is all about https://dxos.org and related technologies.',
136
+ '',
124
137
  ),
125
138
 
126
139
  table: str(
127
- '# Table',
140
+ '### Tables',
128
141
  '',
129
142
  `| ${faker.lorem.word().padStart(12)} | ${faker.lorem.word().padStart(12)} | ${faker.lorem.word().padStart(12)} |`,
130
143
  `|-${''.padStart(12, '-')}-|-${''.padStart(12, '-')}-|-${''.padStart(12, '-')}-|`,
@@ -134,49 +147,56 @@ const text = {
134
147
  '',
135
148
  ),
136
149
 
137
- image: str('# Image', '', img),
150
+ image: str('### Image', '', img),
138
151
 
139
152
  headings: str(
140
153
  ...[1, 2, 3, 4, 5, 6].map((level) => ['#'.repeat(level) + ` Heading ${level}`, faker.lorem.sentences(), '']).flat(),
141
154
  ),
142
155
 
156
+ formatting: str('### Formatting', 'This this is **bold**, ~~strikethrough~~, _italic_, and `f(INLINE)`.'),
157
+
158
+ blockquotes: str(
159
+ '### Blockquotes',
160
+ '> This is a block quote.',
161
+ '',
162
+ '> This is a long wrapping block quote. Neque reiciendis ullam quae error labore sit, at, et, nulla, aut at nostrum omnis quas nostrum, at consectetur vitae eos asperiores non omnis ullam in beatae at vitae deserunt asperiores sapiente.',
163
+ '',
164
+ '> This is',
165
+ '> a multi-line',
166
+ '> block quote.',
167
+ ),
168
+
143
169
  paragraphs: str(...faker.helpers.multiple(() => [faker.lorem.paragraph(), ''], { count: 3 }).flat()),
144
170
 
145
171
  footer: str('', '', '', '', ''),
146
172
  };
147
173
 
148
- const document = str(
174
+ const text = str(
149
175
  '# Markdown',
176
+ 'Composer Markdown Editor',
150
177
  '',
151
- '> This is a block quote.',
152
- '',
153
- '> This is a long wrapping block quote. Neque reiciendis ullam quae error labore sit, at, et, nulla, aut at nostrum omnis quas nostrum, at consectetur vitae eos asperiores non omnis ullam in beatae at vitae deserunt asperiores sapiente.',
154
- '',
155
- '> This is',
156
- '> a multi-line',
157
- '> block quote.',
158
- '',
159
- 'This is all about https://dxos.org and related technologies.',
160
- '',
161
- 'This this is **bold**, ~~strikethrough~~, _italic_, and `f(INLINE)`.',
162
- '',
163
- '---',
164
- text.links,
165
- '---',
166
- text.list,
167
- '---',
168
- text.tasks,
169
- '---',
170
- text.numbered,
178
+
171
179
  '---',
172
- text.code,
180
+ '## Basics',
181
+ content.blockquotes,
182
+ content.formatting,
183
+ content.links,
184
+
173
185
  '---',
174
- text.headings,
186
+ '## Lists',
187
+ content.bullets,
188
+ content.tasks,
189
+ content.numbered,
190
+
175
191
  '---',
176
- text.table,
192
+ content.headings,
193
+
177
194
  '---',
178
- text.image,
179
- text.footer,
195
+ '## Misc',
196
+ content.code,
197
+ content.table,
198
+ content.image,
199
+ content.footer,
180
200
  );
181
201
 
182
202
  const links = [
@@ -249,67 +269,73 @@ type StoryProps = {
249
269
  const Story = ({
250
270
  id = 'editor-' + PublicKey.random().toHex().slice(0, 8),
251
271
  text,
252
- extensions: _extensions = [],
272
+ extensions,
253
273
  readonly,
254
274
  placeholder = 'New document.',
255
275
  selection,
256
276
  }: StoryProps) => {
257
- const [object] = useState(createEchoObject(create(TextType, { content: text ?? '' })));
277
+ const [object] = useState(createEchoObject(create(Expando, { content: text ?? '' })));
258
278
  const { themeMode } = useThemeContext();
259
- const extensions = useMemo(
260
- () => [
261
- createBasicExtensions({ readonly, placeholder }),
262
- createMarkdownExtensions({ themeMode }),
263
- createThemeExtensions({
264
- themeMode,
265
- slots: {
266
- editor: { className: 'min-bs-dvh px-8 bg-white dark:bg-black' },
267
- },
268
- }),
269
- createDataExtensions({ id, text: createDocAccessor(object, ['content']) }),
270
- _extensions,
271
- ],
272
- [_extensions, object],
273
- );
274
-
275
279
  const { parentRef, focusAttributes } = useTextEditor(
276
- () => ({ id, initialValue: text, extensions, selection }),
277
- [extensions],
280
+ () => ({
281
+ id,
282
+ initialValue: text,
283
+ extensions: [
284
+ createDataExtensions({ id, text: createDocAccessor(object, ['content']) }),
285
+ createBasicExtensions({ readonly, placeholder }),
286
+ createMarkdownExtensions({ themeMode }),
287
+ createThemeExtensions({
288
+ themeMode,
289
+ slots: {
290
+ content: {
291
+ className: editorContent,
292
+ },
293
+ },
294
+ }),
295
+ extensions || [],
296
+ ],
297
+ selection,
298
+ }),
299
+ [object, extensions, themeMode],
278
300
  );
279
301
 
280
- return <div role='none' ref={parentRef} className={mx(textBlockWidth, 'min-bs-dvh')} {...focusAttributes} />;
302
+ return <div role='none' className='flex w-full overflow-hidden' ref={parentRef} {...focusAttributes} />;
281
303
  };
282
304
 
283
305
  export default {
284
- title: 'react-ui-editor/useTextEditor',
285
- decorators: [withTheme],
306
+ title: 'react-ui-editor/TextEditor',
307
+ decorators: [withTheme, withFullscreen()],
286
308
  render: Story,
287
309
  parameters: { translations, layout: 'fullscreen' },
288
310
  };
289
311
 
290
- // TODO(burdon): Test invalid inputs (e.g., selection).
291
-
292
312
  const defaults = [
293
313
  autocomplete({
294
314
  onSearch: (text) => links.filter(({ label }) => label.toLowerCase().includes(text.toLowerCase())),
295
315
  }),
296
- decorateMarkdown({ renderLinkButton, selectionChangeDelay: 100 }),
316
+ decorateMarkdown({ renderLinkButton, selectionChangeDelay: 100, numberedHeadings: { from: 1, to: 4 } }),
297
317
  formattingKeymap(),
298
- image(),
299
- table(),
300
318
  linkTooltip(renderLinkTooltip),
301
319
  ];
302
320
 
303
321
  export const Default = {
304
- render: () => <Story text={document} extensions={defaults} />,
322
+ render: () => <Story text={text} extensions={defaults} />,
305
323
  };
306
324
 
307
325
  export const Readonly = {
308
- render: () => <Story text={document} extensions={defaults} readonly />,
326
+ render: () => <Story text={text} extensions={defaults} readonly />,
327
+ };
328
+
329
+ export const Empty = {
330
+ render: () => <Story extensions={defaults} />,
309
331
  };
310
332
 
311
333
  export const NoExtensions = {
312
- render: () => <Story text={document} />,
334
+ render: () => <Story text={text} />,
335
+ };
336
+
337
+ export const Folding = {
338
+ render: () => <Story text={text} extensions={[folding()]} />,
313
339
  };
314
340
 
315
341
  const large = faker.helpers.multiple(() => faker.lorem.paragraph({ min: 8, max: 16 }), { count: 20 }).join('\n\n');
@@ -319,8 +345,14 @@ const largeWithImages = faker.helpers
319
345
  .flatMap((x) => x)
320
346
  .join('\n\n');
321
347
 
322
- export const Empty = {
323
- render: () => <Story />,
348
+ const headings = str(
349
+ ...[1, 2, 2, 3, 3, 4, 4, 4, 5, 5, 2, 3, 3, 2, 2, 6, 1]
350
+ .map((level) => ['#'.repeat(level) + ' ' + faker.lorem.sentence(3), faker.lorem.sentences(), ''])
351
+ .flat(),
352
+ );
353
+
354
+ export const Headings = {
355
+ render: () => <Story text={headings} extensions={decorateMarkdown({ numberedHeadings: { from: 2, to: 4 } })} />,
324
356
  };
325
357
 
326
358
  const global = new Map<string, SelectionState>();
@@ -342,31 +374,46 @@ export const ScrollingWithImages = {
342
374
  };
343
375
 
344
376
  export const Links = {
345
- render: () => <Story text={str(text.links, text.footer)} extensions={[linkTooltip(renderLinkTooltip)]} />,
377
+ render: () => <Story text={str(content.links, content.footer)} extensions={[linkTooltip(renderLinkTooltip)]} />,
346
378
  };
347
379
 
348
380
  export const Image = {
349
- render: () => <Story text={str(text.image, text.footer)} extensions={[image()]} />,
381
+ render: () => <Story text={str(content.image, content.footer)} extensions={[image()]} />,
350
382
  };
351
383
 
352
384
  export const Code = {
353
- render: () => <Story text={str(text.code, text.footer)} extensions={[decorateMarkdown()]} />,
385
+ render: () => <Story text={str(content.code, content.footer)} extensions={[decorateMarkdown()]} />,
354
386
  };
355
387
 
356
388
  export const Lists = {
357
389
  render: () => (
358
- <Story text={str(text.tasks, '', text.list, '', text.numbered, text.footer)} extensions={[decorateMarkdown()]} />
390
+ <Story
391
+ text={str(content.tasks, '', content.bullets, '', content.numbered, content.footer)}
392
+ extensions={[decorateMarkdown()]}
393
+ />
359
394
  ),
360
395
  };
361
396
 
397
+ export const BulletList = {
398
+ render: () => <Story text={str(content.bullets, content.footer)} extensions={[decorateMarkdown()]} />,
399
+ };
400
+
401
+ export const OrderedList = {
402
+ render: () => <Story text={str(content.numbered, content.footer)} extensions={[decorateMarkdown()]} />,
403
+ };
404
+
405
+ export const TaskList = {
406
+ render: () => <Story text={str(content.tasks, content.footer)} extensions={[decorateMarkdown()]} />,
407
+ };
408
+
362
409
  export const Table = {
363
- render: () => <Story text={str(text.table, text.footer)} extensions={[table()]} />,
410
+ render: () => <Story text={str(content.table, content.footer)} extensions={[table()]} />,
364
411
  };
365
412
 
366
413
  export const Autocomplete = {
367
414
  render: () => (
368
415
  <Story
369
- text={str('# Autocomplete', '', 'Press Ctrl-Space...', text.footer)}
416
+ text={str('# Autocomplete', '', 'Press Ctrl-Space...', content.footer)}
370
417
  extensions={[
371
418
  decorateMarkdown({ renderLinkButton }),
372
419
  autocomplete({
@@ -380,7 +427,7 @@ export const Autocomplete = {
380
427
  export const CommentedOut = {
381
428
  render: () => (
382
429
  <Story
383
- text={str('# Commented out', '', text.comment, text.footer)}
430
+ text={str('# Commented out', '', content.comment, content.footer)}
384
431
  extensions={[
385
432
  decorateMarkdown(),
386
433
  markdown(),
@@ -393,7 +440,7 @@ export const CommentedOut = {
393
440
  export const Mention = {
394
441
  render: () => (
395
442
  <Story
396
- text={str('# Mention', '', 'Type @...', text.footer)}
443
+ text={str('# Mention', '', 'Type @...', content.footer)}
397
444
  extensions={[
398
445
  mention({
399
446
  onSearch: (text) => names.filter((name) => name.toLowerCase().startsWith(text.toLowerCase())),
@@ -466,7 +513,7 @@ export const Comments = {
466
513
  const _comments = useSignal<Comment[]>([]);
467
514
  return (
468
515
  <Story
469
- text={str('# Comments', '', text.paragraphs, text.footer)}
516
+ text={str('# Comments', '', content.paragraphs, content.footer)}
470
517
  extensions={[
471
518
  createExternalCommentSync(
472
519
  'test',
@@ -504,7 +551,7 @@ export const Comments = {
504
551
  export const Vim = {
505
552
  render: () => (
506
553
  <Story
507
- text={str('# Vim Mode', '', 'The distant future. The year 2000.', '', text.paragraphs)}
554
+ text={str('# Vim Mode', '', 'The distant future. The year 2000.', '', content.paragraphs)}
508
555
  extensions={[defaults, InputModeExtensions.vim]}
509
556
  />
510
557
  ),
@@ -534,7 +581,7 @@ const typewriterItems = localStorage.getItem('dxos.org/plugin/markdown/typewrite
534
581
  export const Listener = {
535
582
  render: () => (
536
583
  <Story
537
- text={str('# Listener', '', text.footer)}
584
+ text={str('# Listener', '', content.footer)}
538
585
  extensions={[
539
586
  listener({
540
587
  onFocus: (focusing) => {
@@ -552,7 +599,7 @@ export const Listener = {
552
599
  export const Typewriter = {
553
600
  render: () => (
554
601
  <Story
555
- text={str('# Typewriter', '', text.paragraphs, text.footer)}
602
+ text={str('# Typewriter', '', content.paragraphs, content.footer)}
556
603
  extensions={[typewriter({ items: typewriterItems })]}
557
604
  />
558
605
  ),
@@ -561,7 +608,7 @@ export const Typewriter = {
561
608
  export const Blast = {
562
609
  render: () => (
563
610
  <Story
564
- text={str('# Blast', '', text.paragraphs, text.code, text.paragraphs)}
611
+ text={str('# Blast', '', content.paragraphs, content.code, content.paragraphs)}
565
612
  extensions={[
566
613
  typewriter({ items: typewriterItems }),
567
614
  blast(
@@ -61,6 +61,8 @@ const ToolbarSeparator = () => <div role='separator' className='grow' />;
61
61
  // Root
62
62
  //
63
63
 
64
+ const [ToolbarContextProvider, useToolbarContext] = createContext<ToolbarProps>('Toolbar');
65
+
64
66
  export type ToolbarProps = ThemedClassName<
65
67
  PropsWithChildren<{
66
68
  state: (Formatting & { comment?: boolean; mode?: EditorViewMode; selection?: boolean }) | undefined;
@@ -68,8 +70,6 @@ export type ToolbarProps = ThemedClassName<
68
70
  }>
69
71
  >;
70
72
 
71
- const [ToolbarContextProvider, useToolbarContext] = createContext<ToolbarProps>('Toolbar');
72
-
73
73
  const ToolbarRoot = ({ children, onAction, classNames, state }: ToolbarProps) => {
74
74
  return (
75
75
  <ToolbarContextProvider onAction={onAction} state={state}>
@@ -320,11 +320,11 @@ const MarkdownHeading = () => {
320
320
  //
321
321
 
322
322
  const markdownStyles: ButtonProps[] = [
323
- { type: 'strong', Icon: TextB, getState: (state) => state.strong },
324
- { type: 'emphasis', Icon: TextItalic, getState: (state) => state.emphasis },
325
- { type: 'strikethrough', Icon: TextStrikethrough, getState: (state) => state.strikethrough },
326
- { type: 'code', Icon: Code, getState: (state) => state.code },
327
- { type: 'link', Icon: Link, getState: (state) => state.link },
323
+ { type: 'strong', Icon: TextB, getState: (state) => !!state?.strong },
324
+ { type: 'emphasis', Icon: TextItalic, getState: (state) => !!state?.emphasis },
325
+ { type: 'strikethrough', Icon: TextStrikethrough, getState: (state) => !!state?.strikethrough },
326
+ { type: 'code', Icon: Code, getState: (state) => !!state?.code },
327
+ { type: 'link', Icon: Link, getState: (state) => !!state?.link },
328
328
  ];
329
329
 
330
330
  const MarkdownStyles = () => {
@@ -380,7 +380,7 @@ const markdownBlocks: ButtonProps[] = [
380
380
  {
381
381
  type: 'blockquote',
382
382
  Icon: Quotes,
383
- getState: (state) => state.blockQuote,
383
+ getState: (state) => !!state?.blockQuote,
384
384
  },
385
385
  {
386
386
  type: 'codeblock',
@@ -482,6 +482,14 @@ const MarkdownCustom = ({ onUpload }: MarkdownCustomOptions = {}) => {
482
482
  const MarkdownActions = () => {
483
483
  const { onAction, state } = useToolbarContext('MarkdownActions');
484
484
  const { t } = useTranslation(translationKey);
485
+
486
+ let toolTipKey = 'comment label';
487
+ if (state?.comment) {
488
+ toolTipKey = 'selection overlaps existing comment label';
489
+ } else if (state?.selection === false) {
490
+ toolTipKey = 'select text to comment label';
491
+ }
492
+
485
493
  return (
486
494
  <>
487
495
  {/* TODO(burdon): Toggle readonly state. */}
@@ -495,7 +503,7 @@ const MarkdownActions = () => {
495
503
  onClick={() => onAction?.({ type: 'comment' })}
496
504
  disabled={!state || state.comment || !state.selection}
497
505
  >
498
- {t('comment label')}
506
+ {t(toolTipKey)}
499
507
  </ToolbarButton>
500
508
  </>
501
509
  );
@@ -0,0 +1,28 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { EditorView } from '@codemirror/view';
6
+
7
+ export { getToken } from './styles';
8
+
9
+ /**
10
+ * CodeMirror content width.
11
+ * 40rem = 640px. Corresponds to initial plank width (Google docs, Stashpad, etc.)
12
+ * 50rem = 800px. Maximum content width for solo mode.
13
+ */
14
+ export const editorContent = '!mt-[16px] !mli-auto w-full max-w-[min(50rem,100%-4rem)]';
15
+
16
+ export const editorWithToolbarLayout =
17
+ 'grid grid-cols-1 grid-rows-[min-content_1fr] data-[toolbar=disabled]:grid-rows-[1fr] justify-center content-start overflow-hidden';
18
+
19
+ export const editorGutter = EditorView.baseTheme({
20
+ '.cm-gutters': {
21
+ // Match margin from content.
22
+ marginTop: '16px',
23
+ // Inside within content margin.
24
+ marginRight: '-32px',
25
+ width: '32px',
26
+ backgroundColor: 'transparent !important',
27
+ },
28
+ });
@@ -20,14 +20,14 @@ export type AutocompleteResult = Completion;
20
20
 
21
21
  export type AutocompleteOptions = {
22
22
  activateOnTyping?: boolean;
23
- onSearch: (text: string) => Completion[];
23
+ onSearch?: (text: string) => Completion[];
24
24
  };
25
25
 
26
26
  /**
27
27
  * Autocomplete extension.
28
28
  */
29
- export const autocomplete = ({ activateOnTyping, onSearch }: AutocompleteOptions) => {
30
- return [
29
+ export const autocomplete = ({ activateOnTyping, onSearch }: AutocompleteOptions = {}) => {
30
+ const extentions = [
31
31
  // https://codemirror.net/docs/ref/#view.keymap
32
32
  // https://discuss.codemirror.net/t/how-can-i-replace-the-default-autocompletion-keymap-v6/3322
33
33
  // TODO(burdon): Set custom keymap.
@@ -44,20 +44,26 @@ export const autocomplete = ({ activateOnTyping, onSearch }: AutocompleteOptions
44
44
  // TODO(burdon): Styles/fragments.
45
45
  tooltipClass: () => 'shadow rounded',
46
46
  }),
47
-
48
- // TODO(burdon): Optional decoration via addToOptions
49
- markdownLanguage.data.of({
50
- autocomplete: (context: CompletionContext): CompletionResult | null => {
51
- const match = context.matchBefore(/\w*/);
52
- if (!match || (match.from === match.to && !context.explicit)) {
53
- return null;
54
- }
55
-
56
- return {
57
- from: match.from,
58
- options: onSearch(match.text.toLowerCase()),
59
- };
60
- },
61
- }),
62
47
  ];
48
+
49
+ if (onSearch) {
50
+ extentions.push(
51
+ // TODO(burdon): Optional decoration via addToOptions
52
+ markdownLanguage.data.of({
53
+ autocomplete: (context: CompletionContext): CompletionResult | null => {
54
+ const match = context.matchBefore(/\w*/);
55
+ if (!match || (match.from === match.to && !context.explicit)) {
56
+ return null;
57
+ }
58
+
59
+ return {
60
+ from: match.from,
61
+ options: onSearch(match.text.toLowerCase()),
62
+ };
63
+ },
64
+ }),
65
+ );
66
+ }
67
+
68
+ return extentions;
63
69
  };
@@ -7,10 +7,9 @@ import '@dxosTheme';
7
7
  import '@preact/signals-react';
8
8
  import React, { useEffect, useState } from 'react';
9
9
 
10
- import { TextType } from '@braneframe/types';
11
10
  import { Repo } from '@dxos/automerge/automerge-repo';
12
11
  import { BroadcastChannelNetworkAdapter } from '@dxos/automerge/automerge-repo-network-broadcastchannel';
13
- import { create, type Expando } from '@dxos/echo-schema';
12
+ import { Expando, create } from '@dxos/echo-schema';
14
13
  import { type PublicKey } from '@dxos/keys';
15
14
  import { Filter, DocAccessor, createDocAccessor, useSpace, useQuery, type Space } from '@dxos/react-client/echo';
16
15
  import { useIdentity, type Identity } from '@dxos/react-client/halo';
@@ -18,6 +17,7 @@ import { ClientRepeater } from '@dxos/react-client/testing';
18
17
  import { useThemeContext } from '@dxos/react-ui';
19
18
  import { withTheme } from '@dxos/storybook-utils';
20
19
 
20
+ import { editorContent } from '../../defaults';
21
21
  import { useTextEditor } from '../../hooks';
22
22
  import translations from '../../translations';
23
23
  import { createBasicExtensions, createDataExtensions, createThemeExtensions } from '../factories';
@@ -45,9 +45,7 @@ const Editor = ({ source, autoFocus, space, identity }: EditorProps) => {
45
45
  createThemeExtensions({
46
46
  themeMode,
47
47
  slots: {
48
- editor: { className: 'w-full bg-white dark:bg-black' },
49
- // TODO(burdon): Sufficient padding so indicator isn't clipped.
50
- content: { className: '!m-8' },
48
+ editor: { className: editorContent },
51
49
  },
52
50
  }),
53
51
  createDataExtensions({ id: 'test', text: source, space, identity }),
@@ -136,7 +134,7 @@ export const WithEcho = {
136
134
  space.db.add(
137
135
  create({
138
136
  type: 'test',
139
- content: create(TextType, { content: initialContent }),
137
+ content: create(Expando, { content: initialContent }),
140
138
  }),
141
139
  );
142
140
  }}
@@ -611,6 +611,10 @@ const hasActiveSelection = (state: EditorState): boolean => {
611
611
  return state.selection.ranges.some((range) => !range.empty);
612
612
  };
613
613
 
614
+ /**
615
+ * Manages external comment synchronization for the editor.
616
+ * This class subscribes to external comment updates and applies them to the editor view.
617
+ */
614
618
  class ExternalCommentSync implements PluginValue {
615
619
  private readonly unsubscribe: () => void;
616
620
 
@@ -30,8 +30,7 @@ import { hexToHue, isNotFalsy } from '@dxos/util';
30
30
 
31
31
  import { automerge } from './automerge';
32
32
  import { awareness, SpaceAwarenessProvider } from './awareness';
33
- import { type ThemeStyles } from '../styles';
34
- import { defaultTheme } from '../themes';
33
+ import { type ThemeStyles, defaultTheme } from '../styles';
35
34
 
36
35
  //
37
36
  // Basic
@@ -146,6 +145,8 @@ const defaultThemeSlots = {
146
145
  },
147
146
  };
148
147
 
148
+ // TODO(burdon): Should only have one baseTheme?
149
+ // https://codemirror.net/examples/styling
149
150
  export const createThemeExtensions = ({ theme, themeMode, slots: _slots }: ThemeExtensionsOptions = {}): Extension => {
150
151
  const slots = defaultsDeep({}, _slots, defaultThemeSlots);
151
152
  return [