@dxos/react-ui-editor 0.6.9 → 0.6.10-main.3cfcc89

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 (75) hide show
  1. package/dist/lib/browser/index.mjs +759 -732
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/InputMode.stories.d.ts +2 -1
  5. package/dist/types/src/InputMode.stories.d.ts.map +1 -1
  6. package/dist/types/src/TextEditor.stories.d.ts +20 -13
  7. package/dist/types/src/TextEditor.stories.d.ts.map +1 -1
  8. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
  9. package/dist/types/src/defaults.d.ts +5 -1
  10. package/dist/types/src/defaults.d.ts.map +1 -1
  11. package/dist/types/src/extensions/autocomplete.d.ts +3 -2
  12. package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
  13. package/dist/types/src/extensions/automerge/automerge.stories.d.ts +2 -1
  14. package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
  15. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  16. package/dist/types/src/extensions/dnd.d.ts.map +1 -1
  17. package/dist/types/src/extensions/factories.d.ts +2 -2
  18. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  19. package/dist/types/src/extensions/folding.d.ts.map +1 -1
  20. package/dist/types/src/extensions/markdown/action.d.ts +1 -1
  21. package/dist/types/src/extensions/markdown/action.d.ts.map +1 -1
  22. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  23. package/dist/types/src/extensions/markdown/changes.d.ts +10 -0
  24. package/dist/types/src/extensions/markdown/changes.d.ts.map +1 -0
  25. package/dist/types/src/extensions/markdown/changes.test.d.ts +2 -0
  26. package/dist/types/src/extensions/markdown/changes.test.d.ts.map +1 -0
  27. package/dist/types/src/extensions/markdown/debug.d.ts +11 -0
  28. package/dist/types/src/extensions/markdown/debug.d.ts.map +1 -0
  29. package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
  30. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
  31. package/dist/types/src/extensions/markdown/index.d.ts +1 -0
  32. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
  33. package/dist/types/src/extensions/markdown/styles.d.ts +4 -0
  34. package/dist/types/src/extensions/markdown/styles.d.ts.map +1 -0
  35. package/dist/types/src/index.d.ts +0 -1
  36. package/dist/types/src/index.d.ts.map +1 -1
  37. package/dist/types/src/styles/theme.d.ts +1 -1
  38. package/dist/types/src/styles/theme.d.ts.map +1 -1
  39. package/dist/types/src/styles/tokens.d.ts +2 -5
  40. package/dist/types/src/styles/tokens.d.ts.map +1 -1
  41. package/dist/types/src/translations.d.ts +1 -0
  42. package/dist/types/src/translations.d.ts.map +1 -1
  43. package/package.json +26 -27
  44. package/src/InputMode.stories.tsx +1 -1
  45. package/src/TextEditor.stories.tsx +125 -77
  46. package/src/components/Toolbar/Toolbar.tsx +91 -92
  47. package/src/defaults.ts +16 -11
  48. package/src/extensions/annotations.ts +2 -2
  49. package/src/extensions/autocomplete.ts +4 -1
  50. package/src/extensions/automerge/automerge.stories.tsx +1 -1
  51. package/src/extensions/awareness/awareness.ts +1 -1
  52. package/src/extensions/comments.ts +11 -45
  53. package/src/extensions/dnd.ts +3 -5
  54. package/src/extensions/factories.ts +8 -8
  55. package/src/extensions/folding.tsx +3 -4
  56. package/src/extensions/markdown/action.ts +1 -0
  57. package/src/extensions/markdown/bundle.ts +3 -1
  58. package/src/extensions/markdown/{link-paste.test.ts → changes.test.ts} +2 -2
  59. package/src/extensions/markdown/changes.ts +148 -0
  60. package/src/extensions/markdown/debug.ts +44 -0
  61. package/src/extensions/markdown/decorate.ts +35 -108
  62. package/src/extensions/markdown/formatting.ts +1 -2
  63. package/src/extensions/markdown/highlight.ts +2 -2
  64. package/src/extensions/markdown/index.ts +1 -0
  65. package/src/extensions/markdown/parser.test.ts +29 -0
  66. package/src/extensions/markdown/styles.ts +103 -0
  67. package/src/index.ts +0 -2
  68. package/src/styles/theme.ts +85 -147
  69. package/src/styles/tokens.ts +6 -6
  70. package/src/translations.ts +1 -0
  71. package/dist/types/src/extensions/markdown/link-paste.d.ts +0 -9
  72. package/dist/types/src/extensions/markdown/link-paste.d.ts.map +0 -1
  73. package/dist/types/src/extensions/markdown/link-paste.test.d.ts +0 -2
  74. package/dist/types/src/extensions/markdown/link-paste.test.d.ts.map +0 -1
  75. package/src/extensions/markdown/link-paste.ts +0 -107
@@ -2,13 +2,16 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import '@dxosTheme';
5
+ import '@dxos-theme';
6
6
 
7
7
  import { markdown } from '@codemirror/lang-markdown';
8
+ import { openSearchPanel } from '@codemirror/search';
9
+ import { type Extension } from '@codemirror/state';
10
+ import { type EditorView } from '@codemirror/view';
8
11
  import { ArrowSquareOut, X } from '@phosphor-icons/react';
9
12
  import { effect, useSignal } from '@preact/signals-react';
10
13
  import defaultsDeep from 'lodash.defaultsdeep';
11
- import React, { type FC, type KeyboardEvent, StrictMode, useState } from 'react';
14
+ import React, { type FC, type KeyboardEvent, useEffect, useState } from 'react';
12
15
  import { createRoot } from 'react-dom/client';
13
16
 
14
17
  import { create, Expando } from '@dxos/echo-schema';
@@ -50,6 +53,8 @@ import {
50
53
  type Comment,
51
54
  type CommentsOptions,
52
55
  type EditorSelectionState,
56
+ debugTree,
57
+ type DebugNode,
53
58
  } from './extensions';
54
59
  import { renderRoot } from './extensions/util';
55
60
  import { useTextEditor, type UseTextEditorProps } from './hooks';
@@ -66,7 +71,7 @@ const img = '![dxos](https://pbs.twimg.com/profile_banners/1268328127673044992/1
66
71
  const content = {
67
72
  tasks: str(
68
73
  //
69
- '### Task List',
74
+ '### TaskList',
70
75
  '',
71
76
  `- [x] ${faker.lorem.sentences()}`,
72
77
  `- [ ] ${faker.lorem.sentences()}`,
@@ -109,7 +114,7 @@ const content = {
109
114
  code: str(
110
115
  '### Code',
111
116
  '',
112
- '```',
117
+ '```bash',
113
118
  '$ ls -las',
114
119
  '```',
115
120
  '',
@@ -199,6 +204,7 @@ const text = str(
199
204
  content.table,
200
205
  content.image,
201
206
  content.footer,
207
+ '=== LAST LINE ===',
202
208
  );
203
209
 
204
210
  const links = [
@@ -212,17 +218,15 @@ const links = [
212
218
  const names = ['adam', 'alice', 'alison', 'bob', 'carol', 'charlie', 'sayuri', 'shoko'];
213
219
 
214
220
  const hover =
215
- 'rounded-sm text-base text-primary-600 hover:text-primary-500 dark:text-primary-300 hover:dark:text-primary-200';
221
+ 'rounded-sm text-baseText text-primary-600 hover:text-primary-500 dark:text-primary-300 hover:dark:text-primary-200';
216
222
 
217
223
  const renderLinkTooltip = (el: Element, url: string) => {
218
224
  const web = new URL(url);
219
225
  createRoot(el).render(
220
- <StrictMode>
221
- <a href={url} target='_blank' rel='noreferrer' className={hover}>
222
- {web.origin}
223
- <ArrowSquareOut weight='bold' className={mx(getSize(4), 'inline-block leading-none mis-1')} />
224
- </a>
225
- </StrictMode>,
226
+ <a href={url} target='_blank' rel='noreferrer' className={hover}>
227
+ {web.origin}
228
+ <ArrowSquareOut weight='bold' className={mx(getSize(4), 'inline-block leading-none mis-1')} />
229
+ </a>,
226
230
  );
227
231
  };
228
232
 
@@ -234,26 +238,22 @@ const Key: FC<{ char: string }> = ({ char }) => (
234
238
 
235
239
  const onCommentsHover: CommentsOptions['onHover'] = (el, shortcut) => {
236
240
  createRoot(el).render(
237
- <StrictMode>
238
- <div className='flex items-center gap-2 px-2 py-2 bg-neutral-700 text-white text-xs rounded'>
239
- <div>Create comment</div>
240
- <div className='flex gap-1'>
241
- {keySymbols(parseShortcut(shortcut)).map((char) => (
242
- <Key key={char} char={char} />
243
- ))}
244
- </div>
241
+ <div className='flex items-center gap-2 px-2 py-2 bg-neutral-700 text-white text-xs rounded'>
242
+ <div>Create comment</div>
243
+ <div className='flex gap-1'>
244
+ {keySymbols(parseShortcut(shortcut)).map((char) => (
245
+ <Key key={char} char={char} />
246
+ ))}
245
247
  </div>
246
- </StrictMode>,
248
+ </div>,
247
249
  );
248
250
  };
249
251
 
250
252
  const renderLinkButton = (el: Element, url: string) => {
251
253
  createRoot(el).render(
252
- <StrictMode>
253
- <a href={url} target='_blank' rel='noreferrer' className={hover}>
254
- <ArrowSquareOut weight='bold' className={mx(getSize(4), 'inline-block leading-none mis-1 mb-[2px]')} />
255
- </a>
256
- </StrictMode>,
254
+ <a href={url} target='_blank' rel='noreferrer' className={hover}>
255
+ <ArrowSquareOut weight='bold' className={mx(getSize(4), 'inline-block leading-none mis-1 mb-[2px]')} />
256
+ </a>,
257
257
  );
258
258
  };
259
259
 
@@ -263,29 +263,34 @@ const renderLinkButton = (el: Element, url: string) => {
263
263
 
264
264
  type StoryProps = {
265
265
  id?: string;
266
+ debug?: boolean;
266
267
  text?: string;
267
268
  readonly?: boolean;
268
269
  placeholder?: string;
270
+ onReady?: (view: EditorView) => void;
269
271
  } & Pick<UseTextEditorProps, 'scrollTo' | 'selection' | 'extensions'>;
270
272
 
271
273
  const Story = ({
272
274
  id = 'editor-' + PublicKey.random().toHex().slice(0, 8),
275
+ debug,
273
276
  text,
274
277
  extensions,
275
278
  readonly,
276
279
  placeholder = 'New document.',
277
280
  scrollTo,
278
281
  selection,
282
+ onReady,
279
283
  }: StoryProps) => {
280
284
  const [object] = useState(createEchoObject(create(Expando, { content: text ?? '' })));
281
285
  const { themeMode } = useThemeContext();
282
- const { parentRef, focusAttributes } = useTextEditor(
286
+ const [tree, setTree] = useState<DebugNode>();
287
+ const { parentRef, focusAttributes, view } = useTextEditor(
283
288
  () => ({
284
289
  id,
285
290
  initialValue: text,
286
291
  extensions: [
287
292
  createDataExtensions({ id, text: createDocAccessor(object, ['content']) }),
288
- createBasicExtensions({ readonly, placeholder }),
293
+ createBasicExtensions({ readonly, placeholder, scrollPastEnd: true }),
289
294
  createMarkdownExtensions({ themeMode }),
290
295
  createThemeExtensions({
291
296
  themeMode,
@@ -296,6 +301,7 @@ const Story = ({
296
301
  },
297
302
  }),
298
303
  extensions || [],
304
+ debug ? debugTree(setTree) : [],
299
305
  ],
300
306
  scrollTo,
301
307
  selection,
@@ -303,7 +309,24 @@ const Story = ({
303
309
  [object, extensions, themeMode],
304
310
  );
305
311
 
306
- return <div role='none' className='flex w-full overflow-hidden' ref={parentRef} {...focusAttributes} />;
312
+ useEffect(() => {
313
+ if (view) {
314
+ onReady?.(view);
315
+ }
316
+ }, [view]);
317
+
318
+ return (
319
+ <div className='flex w-full'>
320
+ <div role='none' className='flex w-full overflow-hidden' ref={parentRef} {...focusAttributes} />
321
+ {debug && (
322
+ <div className='w-[800px] border-l border-separator overflow-auto'>
323
+ <pre className='p-1 font-mono text-xs text-green-800 dark:text-green-200'>
324
+ {JSON.stringify(tree, null, 2)}
325
+ </pre>
326
+ </div>
327
+ )}
328
+ </div>
329
+ );
307
330
  };
308
331
 
309
332
  export default {
@@ -313,7 +336,7 @@ export default {
313
336
  parameters: { translations, layout: 'fullscreen' },
314
337
  };
315
338
 
316
- const defaults = [
339
+ const defaultExtensions: Extension[] = [
317
340
  autocomplete({
318
341
  onSearch: (text) => links.filter(({ label }) => label.toLowerCase().includes(text.toLowerCase())),
319
342
  }),
@@ -323,37 +346,34 @@ const defaults = [
323
346
  ];
324
347
 
325
348
  export const Default = {
326
- render: () => <Story text={text} extensions={defaults} selection={{ anchor: 99, head: 110 }} />,
349
+ render: () => <Story text={text} extensions={defaultExtensions} selection={{ anchor: 99, head: 110 }} />,
327
350
  };
328
351
 
329
- export const ScrollTo = {
330
- render: () => {
331
- // NOTE: Selection won't appear if text is reformatted.
332
- const word = 'Scroll to here...';
333
- const text = str('# Scroll To', longText, '', word, '', longText);
334
- const idx = text.indexOf(word);
335
- return (
336
- <Story text={text} extensions={defaults} scrollTo={idx} selection={{ anchor: idx, head: idx + word.length }} />
337
- );
338
- },
352
+ export const Empty = {
353
+ render: () => <Story extensions={defaultExtensions} />,
339
354
  };
340
355
 
341
356
  export const Readonly = {
342
- render: () => <Story text={text} extensions={defaults} readonly />,
343
- };
344
-
345
- export const Empty = {
346
- render: () => <Story extensions={defaults} />,
357
+ render: () => <Story text={text} extensions={defaultExtensions} readonly />,
347
358
  };
348
359
 
349
360
  export const NoExtensions = {
350
361
  render: () => <Story text={text} />,
351
362
  };
352
363
 
353
- export const Folding = {
354
- render: () => <Story text={text} extensions={[editorGutter, folding()]} />,
364
+ export const Vim = {
365
+ render: () => (
366
+ <Story
367
+ text={str('# Vim Mode', '', 'The distant future. The year 2000.', '', content.paragraphs)}
368
+ extensions={[defaultExtensions, InputModeExtensions.vim]}
369
+ />
370
+ ),
355
371
  };
356
372
 
373
+ //
374
+ // Scrolling
375
+ //
376
+
357
377
  const longText = faker.helpers.multiple(() => faker.lorem.paragraph({ min: 8, max: 16 }), { count: 20 }).join('\n\n');
358
378
 
359
379
  const largeWithImages = faker.helpers
@@ -367,12 +387,12 @@ const headings = str(
367
387
  .flat(),
368
388
  );
369
389
 
370
- export const Headings = {
371
- render: () => <Story text={headings} extensions={decorateMarkdown({ numberedHeadings: { from: 2, to: 4 } })} />,
372
- };
373
-
374
390
  const global = new Map<string, EditorSelectionState>();
375
391
 
392
+ export const Folding = {
393
+ render: () => <Story text={text} extensions={[editorGutter, folding()]} />,
394
+ };
395
+
376
396
  export const Scrolling = {
377
397
  render: () => (
378
398
  <Story
@@ -386,7 +406,34 @@ export const Scrolling = {
386
406
  };
387
407
 
388
408
  export const ScrollingWithImages = {
389
- render: () => <Story text={str('# Large Document', '', largeWithImages)} extensions={[image()]} />,
409
+ render: () => (
410
+ <Story text={str('# Large Document', '', largeWithImages)} extensions={[decorateMarkdown(), image()]} />
411
+ ),
412
+ };
413
+
414
+ export const ScrollTo = {
415
+ render: () => {
416
+ // NOTE: Selection won't appear if text is reformatted.
417
+ const word = 'Scroll to here...';
418
+ const text = str('# Scroll To', longText, '', word, '', longText);
419
+ const idx = text.indexOf(word);
420
+ return (
421
+ <Story
422
+ text={text}
423
+ extensions={defaultExtensions}
424
+ scrollTo={idx}
425
+ selection={{ anchor: idx, head: idx + word.length }}
426
+ />
427
+ );
428
+ },
429
+ };
430
+
431
+ //
432
+ // Markdown
433
+ //
434
+
435
+ export const Headings = {
436
+ render: () => <Story text={headings} extensions={decorateMarkdown({ numberedHeadings: { from: 2, to: 4 } })} />,
390
437
  };
391
438
 
392
439
  export const Links = {
@@ -419,35 +466,39 @@ export const OrderedList = {
419
466
  };
420
467
 
421
468
  export const TaskList = {
422
- render: () => <Story text={str(content.tasks, content.footer)} extensions={[decorateMarkdown()]} />,
469
+ render: () => <Story text={str(content.tasks, content.footer)} extensions={[decorateMarkdown()]} debug />,
423
470
  };
424
471
 
425
472
  export const Table = {
426
- render: () => <Story text={str(content.table, content.footer)} extensions={[table()]} />,
473
+ render: () => <Story text={str(content.table, content.footer)} extensions={[decorateMarkdown(), table()]} />,
427
474
  };
428
475
 
429
- export const Autocomplete = {
476
+ export const CommentedOut = {
430
477
  render: () => (
431
478
  <Story
432
- text={str('# Autocomplete', '', 'Press Ctrl-Space...', content.footer)}
479
+ text={str('# Commented out', '', content.comment, content.footer)}
433
480
  extensions={[
434
- decorateMarkdown({ renderLinkButton }),
435
- autocomplete({
436
- onSearch: (text) => links.filter(({ label }) => label.toLowerCase().includes(text.toLowerCase())),
437
- }),
481
+ decorateMarkdown(),
482
+ markdown(),
483
+ // commentBlock()
438
484
  ]}
439
485
  />
440
486
  ),
441
487
  };
442
488
 
443
- export const CommentedOut = {
489
+ //
490
+ // Custom
491
+ //
492
+
493
+ export const Autocomplete = {
444
494
  render: () => (
445
495
  <Story
446
- text={str('# Commented out', '', content.comment, content.footer)}
496
+ text={str('# Autocomplete', '', 'Press Ctrl-Space...', content.footer)}
447
497
  extensions={[
448
- decorateMarkdown(),
449
- markdown(),
450
- // commentBlock()
498
+ decorateMarkdown({ renderLinkButton }),
499
+ autocomplete({
500
+ onSearch: (text) => links.filter(({ label }) => label.toLowerCase().includes(text.toLowerCase())),
501
+ }),
451
502
  ]}
452
503
  />
453
504
  ),
@@ -466,7 +517,13 @@ export const Mention = {
466
517
  ),
467
518
  };
468
519
 
469
- const CommandDialog: FC<{ onClose: (action?: CommandAction) => void }> = ({ onClose }) => {
520
+ export const Search = {
521
+ render: () => (
522
+ <Story text={str('# Search', text)} extensions={defaultExtensions} onReady={(view) => openSearchPanel(view)} />
523
+ ),
524
+ };
525
+
526
+ const CommandDialog = ({ onClose }: { onClose: (action?: CommandAction) => void }) => {
470
527
  const [text, setText] = useState('');
471
528
  const handleInsert = () => {
472
529
  onClose(text.length ? { insert: text + '\n' } : undefined);
@@ -561,15 +618,6 @@ export const Comments = {
561
618
  },
562
619
  };
563
620
 
564
- export const Vim = {
565
- render: () => (
566
- <Story
567
- text={str('# Vim Mode', '', 'The distant future. The year 2000.', '', content.paragraphs)}
568
- extensions={[defaults, InputModeExtensions.vim]}
569
- />
570
- ),
571
- };
572
-
573
621
  export const Annotations = {
574
622
  render: () => <Story text={str('# Annotations', '', longText)} extensions={[annotations({ match: /volup/gi })]} />,
575
623
  };
@@ -589,8 +637,6 @@ export const DND = {
589
637
  ),
590
638
  };
591
639
 
592
- const typewriterItems = localStorage.getItem('dxos.org/plugin/markdown/typewriter')?.split(',');
593
-
594
640
  export const Listener = {
595
641
  render: () => (
596
642
  <Story
@@ -609,6 +655,8 @@ export const Listener = {
609
655
  ),
610
656
  };
611
657
 
658
+ const typewriterItems = localStorage.getItem('dxos.org/plugin/markdown/typewriter')?.split(',');
659
+
612
660
  export const Typewriter = {
613
661
  render: () => (
614
662
  <Story
@@ -12,6 +12,7 @@ import {
12
12
  ListBullets,
13
13
  ListChecks,
14
14
  ListNumbers,
15
+ MagnifyingGlass,
15
16
  Paragraph,
16
17
  Quotes,
17
18
  TextStrikethrough,
@@ -140,89 +141,6 @@ const ToolbarButton = ({ Icon, children, ...props }: ToolbarButtonProps) => {
140
141
  );
141
142
  };
142
143
 
143
- //
144
- // View Mode
145
- //
146
-
147
- const ViewModeIcons: Record<EditorViewMode, Icon> = {
148
- preview: PencilSimple,
149
- readonly: PencilSimpleSlash,
150
- source: MarkdownLogo,
151
- };
152
-
153
- const MarkdownView = ({ mode }: { mode: EditorViewMode }) => {
154
- const { t } = useTranslation(translationKey);
155
- const { onAction } = useToolbarContext('ViewMode');
156
- const ModeIcon = ViewModeIcons[mode ?? 'preview'];
157
- const suppressNextTooltip = useRef<boolean>(false);
158
- const [tooltipOpen, setTooltipOpen] = useState<boolean>(false);
159
- const [selectOpen, setSelectOpen] = useState<boolean>(false);
160
- return (
161
- <Tooltip.Root
162
- open={tooltipOpen}
163
- onOpenChange={(nextOpen) => {
164
- if (nextOpen && suppressNextTooltip.current) {
165
- suppressNextTooltip.current = false;
166
- return setTooltipOpen(false);
167
- } else {
168
- return setTooltipOpen(nextOpen);
169
- }
170
- }}
171
- >
172
- {/* TODO(thure): `Select` encounters a ref error if used here (repro: select a heading, then select another
173
- heading). Determine the root cause and fix or report to Radix. */}
174
- <DropdownMenu.Root
175
- open={selectOpen}
176
- onOpenChange={(nextOpen: boolean) => {
177
- if (!nextOpen) {
178
- suppressNextTooltip.current = true;
179
- }
180
- return setSelectOpen(nextOpen);
181
- }}
182
- >
183
- <Tooltip.Trigger asChild>
184
- <NaturalToolbar.Button asChild>
185
- <DropdownMenu.Trigger asChild>
186
- <Button variant='ghost' classNames={buttonStyles}>
187
- <span className='sr-only'>{t('mode label')}</span>
188
- <ModeIcon className={iconStyles} />
189
- <CaretDown />
190
- </Button>
191
- </DropdownMenu.Trigger>
192
- </NaturalToolbar.Button>
193
- </Tooltip.Trigger>
194
- <DropdownMenu.Portal>
195
- <DropdownMenu.Content classNames='is-min md:is-min' onCloseAutoFocus={(e) => e.preventDefault()}>
196
- <DropdownMenu.Viewport>
197
- {EditorViewModes.map((value) => {
198
- const Icon = ViewModeIcons[value];
199
- return (
200
- <DropdownMenu.CheckboxItem
201
- key={value}
202
- checked={value === mode}
203
- onClick={() => onAction?.({ type: 'view-mode', data: value })}
204
- >
205
- <Icon className={iconStyles} />
206
- <span className='whitespace-nowrap grow'>{t(`${value} mode label`)}</span>
207
- <Check className={value === mode ? 'visible' : 'invisible'} />
208
- </DropdownMenu.CheckboxItem>
209
- );
210
- })}
211
- </DropdownMenu.Viewport>
212
- <DropdownMenu.Arrow />
213
- </DropdownMenu.Content>
214
- </DropdownMenu.Portal>
215
- </DropdownMenu.Root>
216
- <Tooltip.Portal>
217
- <Tooltip.Content {...tooltipProps}>
218
- {t('view mode label')}
219
- <Tooltip.Arrow />
220
- </Tooltip.Content>
221
- </Tooltip.Portal>
222
- </Tooltip.Root>
223
- );
224
- };
225
-
226
144
  //
227
145
  // Heading
228
146
  //
@@ -474,28 +392,109 @@ const MarkdownCustom = ({ onUpload }: MarkdownCustomOptions = {}) => {
474
392
  );
475
393
  };
476
394
 
395
+ //
396
+ // View Mode
397
+ //
398
+
399
+ const ViewModeIcons: Record<EditorViewMode, Icon> = {
400
+ preview: PencilSimple,
401
+ readonly: PencilSimpleSlash,
402
+ source: MarkdownLogo,
403
+ };
404
+
405
+ const MarkdownView = ({ mode }: { mode: EditorViewMode }) => {
406
+ const { t } = useTranslation(translationKey);
407
+ const { onAction } = useToolbarContext('ViewMode');
408
+ const ModeIcon = ViewModeIcons[mode ?? 'preview'];
409
+ const suppressNextTooltip = useRef<boolean>(false);
410
+ const [tooltipOpen, setTooltipOpen] = useState<boolean>(false);
411
+ const [selectOpen, setSelectOpen] = useState<boolean>(false);
412
+ return (
413
+ <Tooltip.Root
414
+ open={tooltipOpen}
415
+ onOpenChange={(nextOpen) => {
416
+ if (nextOpen && suppressNextTooltip.current) {
417
+ suppressNextTooltip.current = false;
418
+ return setTooltipOpen(false);
419
+ } else {
420
+ return setTooltipOpen(nextOpen);
421
+ }
422
+ }}
423
+ >
424
+ {/* TODO(thure): `Select` encounters a ref error if used here (repro: select a heading, then select another
425
+ heading). Determine the root cause and fix or report to Radix. */}
426
+ <DropdownMenu.Root
427
+ open={selectOpen}
428
+ onOpenChange={(nextOpen: boolean) => {
429
+ if (!nextOpen) {
430
+ suppressNextTooltip.current = true;
431
+ }
432
+ return setSelectOpen(nextOpen);
433
+ }}
434
+ >
435
+ <Tooltip.Trigger asChild>
436
+ <NaturalToolbar.Button asChild>
437
+ <DropdownMenu.Trigger asChild>
438
+ <Button variant='ghost' classNames={buttonStyles}>
439
+ <span className='sr-only'>{t('mode label')}</span>
440
+ <ModeIcon className={iconStyles} />
441
+ <CaretDown />
442
+ </Button>
443
+ </DropdownMenu.Trigger>
444
+ </NaturalToolbar.Button>
445
+ </Tooltip.Trigger>
446
+ <DropdownMenu.Portal>
447
+ <DropdownMenu.Content classNames='is-min md:is-min' onCloseAutoFocus={(e) => e.preventDefault()}>
448
+ <DropdownMenu.Viewport>
449
+ {EditorViewModes.map((value) => {
450
+ const Icon = ViewModeIcons[value];
451
+ return (
452
+ <DropdownMenu.CheckboxItem
453
+ key={value}
454
+ checked={value === mode}
455
+ onClick={() => onAction?.({ type: 'view-mode', data: value })}
456
+ >
457
+ <Icon className={iconStyles} />
458
+ <span className='whitespace-nowrap grow'>{t(`${value} mode label`)}</span>
459
+ <Check className={value === mode ? 'visible' : 'invisible'} />
460
+ </DropdownMenu.CheckboxItem>
461
+ );
462
+ })}
463
+ </DropdownMenu.Viewport>
464
+ <DropdownMenu.Arrow />
465
+ </DropdownMenu.Content>
466
+ </DropdownMenu.Portal>
467
+ </DropdownMenu.Root>
468
+ <Tooltip.Portal>
469
+ <Tooltip.Content {...tooltipProps}>
470
+ {t('view mode label')}
471
+ <Tooltip.Arrow />
472
+ </Tooltip.Content>
473
+ </Tooltip.Portal>
474
+ </Tooltip.Root>
475
+ );
476
+ };
477
+
477
478
  //
478
479
  // Actions
479
480
  //
480
481
 
481
- // TODO(burdon): Make extensible.
482
482
  const MarkdownActions = () => {
483
483
  const { onAction, state } = useToolbarContext('MarkdownActions');
484
484
  const { t } = useTranslation(translationKey);
485
485
 
486
- let toolTipKey = 'comment label';
486
+ let commentToolTipKey = 'comment label';
487
487
  if (state?.comment) {
488
- toolTipKey = 'selection overlaps existing comment label';
488
+ commentToolTipKey = 'selection overlaps existing comment label';
489
489
  } else if (state?.selection === false) {
490
- toolTipKey = 'select text to comment label';
490
+ commentToolTipKey = 'select text to comment label';
491
491
  }
492
492
 
493
493
  return (
494
494
  <>
495
- {/* TODO(burdon): Toggle readonly state. */}
496
- {/* <ToolbarButton value='comment' Icon={BookOpenText} onClick={() => onAction?.({ type: 'comment' })}> */}
497
- {/* {t('comment label')} */}
498
- {/* </ToolbarButton> */}
495
+ <ToolbarButton value='search' Icon={MagnifyingGlass} onClick={() => onAction?.({ type: 'search' })}>
496
+ {t('search label')}
497
+ </ToolbarButton>
499
498
  <ToolbarButton
500
499
  value='comment'
501
500
  Icon={ChatText}
@@ -503,7 +502,7 @@ const MarkdownActions = () => {
503
502
  onClick={() => onAction?.({ type: 'comment' })}
504
503
  disabled={!state || state.comment || !state.selection}
505
504
  >
506
- {t(toolTipKey)}
505
+ {t(commentToolTipKey)}
507
506
  </ToolbarButton>
508
507
  </>
509
508
  );
package/src/defaults.ts CHANGED
@@ -4,32 +4,37 @@
4
4
 
5
5
  import { EditorView } from '@codemirror/view';
6
6
 
7
- import { getToken } from './styles';
7
+ import { mx } from '@dxos/react-ui-theme';
8
+
9
+ import { fontMono } from './styles';
10
+
11
+ const margin = '!mt-[16px]';
8
12
 
9
13
  /**
10
14
  * CodeMirror content width.
11
15
  * 40rem = 640px. Corresponds to initial plank width (Google docs, Stashpad, etc.)
12
16
  * 50rem = 800px. Maximum content width for solo mode.
13
17
  */
14
- export const editorContent = '!mt-[16px] !mb-[32px] !mli-auto w-full max-w-[min(50rem,100%-4rem)]';
18
+ export const editorContent = mx(margin, '!mli-auto w-full max-w-[min(50rem,100%-2rem)]');
19
+
20
+ /**
21
+ * Margin for numbers.
22
+ */
23
+ export const editorFullWidth = mx(margin, '!ml-[3rem]');
15
24
 
16
25
  export const editorWithToolbarLayout =
17
26
  'grid grid-cols-1 grid-rows-[min-content_1fr] data-[toolbar=disabled]:grid-rows-[1fr] justify-center content-start overflow-hidden';
18
27
 
19
- export const editorGutter = EditorView.baseTheme({
28
+ // TODO(burdon): Define scrollMargins for fixed gutter.
29
+ export const editorGutter = EditorView.theme({
30
+ // Match margin from content.
20
31
  '.cm-gutters': {
21
- // Match margin from content.
22
32
  marginTop: '16px',
23
- marginBottom: '16px',
24
- // Inside within content margin.
25
- marginRight: '-32px',
26
- width: '32px',
27
- backgroundColor: 'transparent !important',
28
33
  },
29
34
  });
30
35
 
31
- export const editorMonospace = EditorView.baseTheme({
36
+ export const editorMonospace = EditorView.theme({
32
37
  '.cm-content': {
33
- fontFamily: `${getToken('fontFamily.mono')} !important`,
38
+ fontFamily: fontMono,
34
39
  },
35
40
  });
@@ -69,10 +69,10 @@ export const annotations = (options: AnnotationOptions = {}): Extension => {
69
69
  ];
70
70
  };
71
71
 
72
- const styles = EditorView.baseTheme({
72
+ const styles = EditorView.theme({
73
73
  '.cm-annotation': {
74
74
  textDecoration: 'underline',
75
75
  textDecorationStyle: 'wavy',
76
- textDecorationColor: 'red',
76
+ textDecorationColor: 'var(--dx-error)',
77
77
  },
78
78
  });