@dxos/react-ui-editor 0.4.8-main.c67d72c → 0.4.8-main.cd60bd2

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 (71) hide show
  1. package/dist/lib/browser/index.mjs +1926 -1743
  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/TextEditor.d.ts +2 -2
  5. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +2 -1
  7. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  8. package/dist/types/src/components/Toolbar/Toolbar.d.ts +12 -11
  9. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
  10. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +4 -2
  11. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
  12. package/dist/types/src/extensions/automerge/automerge.stories.d.ts +1 -0
  13. package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
  14. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  15. package/dist/types/src/extensions/dnd.d.ts +7 -1
  16. package/dist/types/src/extensions/dnd.d.ts.map +1 -1
  17. package/dist/types/src/extensions/factories.d.ts +5 -1
  18. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  19. package/dist/types/src/extensions/index.d.ts +1 -1
  20. package/dist/types/src/extensions/index.d.ts.map +1 -1
  21. package/dist/types/src/extensions/markdown/action.d.ts +9 -0
  22. package/dist/types/src/extensions/markdown/action.d.ts.map +1 -0
  23. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  24. package/dist/types/src/extensions/markdown/formatting.d.ts +7 -4
  25. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
  26. package/dist/types/src/extensions/markdown/image.d.ts +6 -0
  27. package/dist/types/src/extensions/markdown/image.d.ts.map +1 -1
  28. package/dist/types/src/extensions/markdown/index.d.ts +1 -0
  29. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
  30. package/dist/types/src/extensions/modes.d.ts +2 -1
  31. package/dist/types/src/extensions/modes.d.ts.map +1 -1
  32. package/dist/types/src/extensions/state.d.ts +20 -0
  33. package/dist/types/src/extensions/state.d.ts.map +1 -0
  34. package/dist/types/src/hooks/useActionHandler.d.ts +2 -2
  35. package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -1
  36. package/dist/types/src/hooks/useDocAccessor.d.ts +4 -2
  37. package/dist/types/src/hooks/useDocAccessor.d.ts.map +1 -1
  38. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  39. package/dist/types/src/hooks/useTextEditor.stories.d.ts +1 -0
  40. package/dist/types/src/hooks/useTextEditor.stories.d.ts.map +1 -1
  41. package/dist/types/src/themes/default.d.ts.map +1 -1
  42. package/dist/types/src/translations.d.ts +1 -0
  43. package/dist/types/src/translations.d.ts.map +1 -1
  44. package/package.json +26 -24
  45. package/src/components/TextEditor/TextEditor.stories.tsx +30 -6
  46. package/src/components/TextEditor/TextEditor.tsx +13 -12
  47. package/src/components/Toolbar/Toolbar.stories.tsx +23 -24
  48. package/src/components/Toolbar/Toolbar.tsx +102 -60
  49. package/src/extensions/automerge/automerge.ts +4 -4
  50. package/src/extensions/command/state.ts +2 -2
  51. package/src/extensions/comments.ts +2 -2
  52. package/src/extensions/dnd.ts +31 -9
  53. package/src/extensions/factories.ts +41 -15
  54. package/src/extensions/index.ts +1 -1
  55. package/src/extensions/markdown/action.ts +112 -0
  56. package/src/extensions/markdown/bundle.ts +8 -11
  57. package/src/extensions/markdown/formatting.test.ts +13 -13
  58. package/src/extensions/markdown/formatting.ts +95 -87
  59. package/src/extensions/markdown/image.ts +6 -0
  60. package/src/extensions/markdown/index.ts +1 -0
  61. package/src/extensions/modes.ts +11 -2
  62. package/src/extensions/state.ts +90 -0
  63. package/src/hooks/useActionHandler.ts +4 -85
  64. package/src/hooks/useDocAccessor.ts +8 -2
  65. package/src/hooks/useTextEditor.stories.tsx +3 -3
  66. package/src/hooks/useTextEditor.ts +3 -0
  67. package/src/themes/default.ts +24 -8
  68. package/src/translations.ts +1 -0
  69. package/dist/types/src/extensions/cursor-line-margin.d.ts +0 -7
  70. package/dist/types/src/extensions/cursor-line-margin.d.ts.map +0 -1
  71. package/src/extensions/cursor-line-margin.ts +0 -94
@@ -9,20 +9,20 @@ import { expect } from 'chai';
9
9
  import { describe, test } from '@dxos/test';
10
10
 
11
11
  import {
12
- setHeading,
12
+ addBlockquote,
13
+ addCodeblock,
14
+ addLink,
15
+ addList,
13
16
  addStyle,
17
+ getFormatting,
14
18
  removeStyle,
15
- addLink,
16
19
  removeLink,
17
- addList,
18
20
  removeList,
19
- addBlockquote,
20
21
  removeBlockquote,
21
- addCodeblock,
22
22
  removeCodeblock,
23
+ setHeading,
23
24
  Inline,
24
25
  List,
25
- getFormatting,
26
26
  type Formatting,
27
27
  } from './formatting';
28
28
 
@@ -204,22 +204,22 @@ describe('removeStyle', () => {
204
204
  });
205
205
 
206
206
  describe('addLink', () => {
207
- testCommand('adds a link', '{}', addLink, '[]({})');
207
+ testCommand('adds a link', '{}', addLink(), '[]({})');
208
208
 
209
- testCommand('adds a link around text', 'hello {world}', addLink, 'hello [world]({})');
209
+ testCommand('adds a link around text', 'hello {world}', addLink(), 'hello [world]({})');
210
210
 
211
- testCommand('clears existing links', '[hello {world}](foo)', addLink, 'hello [world]({})');
211
+ testCommand('clears existing links', '[hello {world}](foo)', addLink(), 'hello [world]({})');
212
212
 
213
- testCommand('does nothing across blocks', '{one\n\ntwo}', addLink, null);
213
+ testCommand('does nothing across blocks', '{one\n\ntwo}', addLink(), null);
214
214
 
215
- testCommand('does nothing in code blocks', '```\n{one}\n```', addLink, null);
215
+ testCommand('does nothing in code blocks', '```\n{one}\n```', addLink(), null);
216
216
 
217
- testCommand('patches up overlapping styles before', '*foo {bar* baz}', addLink, '*foo* [*bar* baz]({})');
217
+ testCommand('patches up overlapping styles before', '*foo {bar* baz}', addLink(), '*foo* [*bar* baz]({})');
218
218
 
219
219
  testCommand(
220
220
  'patches up overlapping styles after',
221
221
  'one {two ~~three} four~~',
222
- addLink,
222
+ addLink(),
223
223
  'one [two ~~three~~]({}) ~~four~~',
224
224
  );
225
225
  });
@@ -15,7 +15,7 @@ import {
15
15
  } from '@codemirror/state';
16
16
  import { EditorView, keymap } from '@codemirror/view';
17
17
  import { type SyntaxNodeRef, type SyntaxNode } from '@lezer/common';
18
- import { useState, useMemo } from 'react';
18
+ import { useMemo, useState } from 'react';
19
19
 
20
20
  // Markdown refs:
21
21
  // https://github.github.com/gfm
@@ -56,7 +56,7 @@ export type Formatting = {
56
56
  listStyle: null | 'ordered' | 'bullet' | 'task';
57
57
  };
58
58
 
59
- export const compareFormatting = (a: Formatting, b: Formatting) =>
59
+ export const formattingEquals = (a: Formatting, b: Formatting) =>
60
60
  a.blockType === b.blockType &&
61
61
  a.strong === b.strong &&
62
62
  a.emphasis === b.emphasis &&
@@ -470,90 +470,94 @@ export const removeLink: StateCommand = ({ state, dispatch }) => {
470
470
  };
471
471
 
472
472
  // Add link markup around the selection
473
- export const addLink: StateCommand = ({ state, dispatch }) => {
474
- const changes = state.changeByRange((range) => {
475
- let { from, to } = range;
476
- const cutStyles: SyntaxNode[] = [];
477
- let okay: boolean | null = null;
478
- // Check whether this range is in a position where a link makes sense
479
- syntaxTree(state).iterate({
480
- from,
481
- to,
482
- enter: (node) => {
483
- if (Object.hasOwn(Textblocks, node.name)) {
484
- // If the selection spans multiple textblocks or is in a
485
- // code block, abort
486
- okay =
487
- Textblocks[node.name] !== 'codeblock' &&
488
- from >= blockContentStart(node) &&
489
- to <= blockContentEnd(node, state.doc);
490
- } else if (Object.hasOwn(InlineMarker, node.name)) {
491
- // Look for inline styles that partially overlap the range.
492
- // Expand the range over them if they start directly
493
- // outside, otherwise mark them for later
494
- const sNode = node.node;
495
- if (node.from < from && node.to <= to) {
496
- if (sNode.firstChild!.to === from) {
497
- from = node.from;
498
- } else {
499
- cutStyles.push(sNode);
500
- }
501
- } else if (node.from >= from && node.to > to) {
502
- if (sNode.lastChild!.from === to) {
503
- to = node.to;
504
- } else {
505
- cutStyles.push(sNode);
473
+ export const addLink =
474
+ ({ url, image }: { url?: string; image?: boolean } = {}): StateCommand =>
475
+ ({ state, dispatch }) => {
476
+ const changes = state.changeByRange((range) => {
477
+ let { from, to } = range;
478
+ const cutStyles: SyntaxNode[] = [];
479
+ let okay: boolean | null = null;
480
+ // Check whether this range is in a position where a link makes sense.
481
+ syntaxTree(state).iterate({
482
+ from,
483
+ to,
484
+ enter: (node) => {
485
+ if (Object.hasOwn(Textblocks, node.name)) {
486
+ // If the selection spans multiple textblocks or is in a code block, abort.
487
+ okay =
488
+ Textblocks[node.name] !== 'codeblock' &&
489
+ from >= blockContentStart(node) &&
490
+ to <= blockContentEnd(node, state.doc);
491
+ } else if (Object.hasOwn(InlineMarker, node.name)) {
492
+ // Look for inline styles that partially overlap the range.
493
+ // Expand the range over them if they start directly outside, otherwise mark them for later.
494
+ const sNode = node.node;
495
+ if (node.from < from && node.to <= to) {
496
+ if (sNode.firstChild!.to === from) {
497
+ from = node.from;
498
+ } else {
499
+ cutStyles.push(sNode);
500
+ }
501
+ } else if (node.from >= from && node.to > to) {
502
+ if (sNode.lastChild!.from === to) {
503
+ to = node.to;
504
+ } else {
505
+ cutStyles.push(sNode);
506
+ }
506
507
  }
507
508
  }
508
- }
509
- },
510
- });
509
+ },
510
+ });
511
511
 
512
- if (okay === null) {
513
- // No textblock found around selection. Check if the rest of the line is empty.
514
- const line = state.doc.lineAt(from);
515
- okay = to <= line.to && !/\S/.test(line.text.slice(from - line.from));
516
- }
517
- if (!okay) {
518
- return { range };
519
- }
512
+ if (okay === null) {
513
+ // No textblock found around selection. Check if the rest of the line is empty.
514
+ const line = state.doc.lineAt(from);
515
+ okay = to <= line.to && !/\S/.test(line.text.slice(from - line.from));
516
+ }
517
+ if (!okay) {
518
+ return { range };
519
+ }
520
520
 
521
- const changes: ChangeSpec[] = [];
522
- // Some changes must be moved to end of change array so that they are applied in the right order
523
- const changesAfter: ChangeSpec[] = [];
524
- // Clear existing links.
525
- removeLinkInner(from, to, changesAfter, state);
526
- let cursorOffset = 1;
527
- // Close and reopen inline styles that partially overlap the range.
528
- for (const style of cutStyles) {
529
- const type = InlineMarker[style.name];
530
- const mark = inlineMarkerText(type);
531
- if (style.from < from) {
532
- // Extends before.
533
- changes.push({ from: skipSpaces(from, state.doc, -1), insert: mark });
534
- changesAfter.push({ from: skipSpaces(from, state.doc, 1, to), insert: mark });
535
- } else {
536
- changes.push({ from: skipSpaces(to, state.doc, -1, from), insert: mark });
537
- const after = skipSpaces(to, state.doc, 1);
538
- if (after === to) {
539
- cursorOffset += mark.length;
521
+ const changes: ChangeSpec[] = [];
522
+ // Some changes must be moved to end of change array so that they are applied in the right order
523
+ const changesAfter: ChangeSpec[] = [];
524
+ // Clear existing links.
525
+ removeLinkInner(from, to, changesAfter, state);
526
+ let cursorOffset = 1;
527
+ // Close and reopen inline styles that partially overlap the range.
528
+ for (const style of cutStyles) {
529
+ const type = InlineMarker[style.name];
530
+ const mark = inlineMarkerText(type);
531
+ if (style.from < from) {
532
+ // Extends before.
533
+ changes.push({ from: skipSpaces(from, state.doc, -1), insert: mark });
534
+ changesAfter.push({ from: skipSpaces(from, state.doc, 1, to), insert: mark });
535
+ } else {
536
+ changes.push({ from: skipSpaces(to, state.doc, -1, from), insert: mark });
537
+ const after = skipSpaces(to, state.doc, 1);
538
+ if (after === to) {
539
+ cursorOffset += mark.length;
540
+ }
541
+ changesAfter.push({ from: after, insert: mark });
540
542
  }
541
- changesAfter.push({ from: after, insert: mark });
542
543
  }
544
+
545
+ // Add the link markup.
546
+ changes.push({ from, insert: image ? '![' : '[' }, { from: to, insert: `](${url ?? ''})` });
547
+ const changeSet = state.changes(changes.concat(changesAfter));
548
+ // Put the cursor between the title or parenthesis.
549
+ return {
550
+ changes: changeSet,
551
+ range: EditorSelection.cursor(changeSet.mapPos(to, 1) - cursorOffset - (url ? url.length + 2 : 0)),
552
+ };
553
+ });
554
+ if (changes.changes.empty) {
555
+ return false;
543
556
  }
544
- // Add the link markup.
545
- changes.push({ from, insert: '[' }, { from: to, insert: ']()' });
546
- const changeSet = state.changes(changes.concat(changesAfter));
547
- // Put the cursor between the parenthesis.
548
- return { changes: changeSet, range: EditorSelection.cursor(changeSet.mapPos(to, 1) - cursorOffset) };
549
- });
550
- if (changes.changes.empty) {
551
- return false;
552
- }
553
557
 
554
- dispatch(state.update(changes, { userEvent: 'format.link.add', scrollIntoView: true }));
555
- return true;
556
- };
558
+ dispatch(state.update(changes, { userEvent: 'format.link.add', scrollIntoView: true }));
559
+ return true;
560
+ };
557
561
 
558
562
  //
559
563
  // Lists
@@ -1057,7 +1061,7 @@ export const getFormatting = (state: EditorState): Formatting => {
1057
1061
  const inline: (boolean | null)[] = [null, null, null, null];
1058
1062
  let link: boolean = false;
1059
1063
  let blockQuote: boolean | null = null;
1060
- // False indicates mixed list styles
1064
+ // False indicates mixed list styles.
1061
1065
  let listStyle: Formatting['listStyle'] | null | false = null;
1062
1066
 
1063
1067
  // Track block context for list/blockquote handling.
@@ -1216,21 +1220,25 @@ export const getFormatting = (state: EditorState): Formatting => {
1216
1220
  };
1217
1221
 
1218
1222
  /**
1219
- * Hook computes the current formatting state.
1223
+ * Hook provides an extension to compute the current formatting state.
1220
1224
  */
1221
- export const useFormattingState = (): [Formatting | null, Extension] => {
1222
- const [state, setState] = useState<Formatting | null>(null);
1225
+ export const useFormattingState = (): [Formatting | undefined, Extension] => {
1226
+ const [state, setState] = useState<Formatting>();
1227
+
1223
1228
  const observer = useMemo(
1224
1229
  () =>
1225
1230
  EditorView.updateListener.of((update) => {
1226
1231
  if (update.docChanged || update.selectionSet) {
1227
- const newState = getFormatting(update.state);
1228
- if (!state || !compareFormatting(state, newState)) {
1229
- setState(newState);
1230
- }
1232
+ setState((prevState) => {
1233
+ const newState = getFormatting(update.state);
1234
+ if (!prevState || !formattingEquals(prevState, newState)) {
1235
+ return newState;
1236
+ }
1237
+ return prevState;
1238
+ });
1231
1239
  }
1232
1240
  }),
1233
- [],
1241
+ [setState],
1234
1242
  );
1235
1243
 
1236
1244
  return [state, observer];
@@ -97,3 +97,9 @@ class ImageWidget extends WidgetType {
97
97
  return img;
98
98
  }
99
99
  }
100
+
101
+ export type ImageUploadOptions = {
102
+ onSelect: () => { url: string };
103
+ };
104
+
105
+ export const imageUpload = (options: ImageOptions = {}) => {};
@@ -2,6 +2,7 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
+ export * from './action';
5
6
  export * from './bundle';
6
7
  export * from './decorate';
7
8
  export * from './formatting';
@@ -5,8 +5,11 @@
5
5
  import { type Extension, Facet } from '@codemirror/state';
6
6
  import { keymap } from '@codemirror/view';
7
7
  import { vim } from '@replit/codemirror-vim';
8
+ import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
8
9
 
9
- export type EditorMode = 'default' | 'vim' | undefined;
10
+ export const focusEvent = 'focus.container';
11
+
12
+ export type EditorMode = 'default' | 'vim' | 'vscode' | undefined;
10
13
 
11
14
  export type EditorConfig = {
12
15
  type: string;
@@ -19,7 +22,13 @@ export const editorMode = Facet.define<EditorConfig, EditorConfig>({
19
22
 
20
23
  export const EditorModes: { [mode: string]: Extension } = {
21
24
  default: [],
25
+ vscode: [
26
+ // https://github.com/replit/codemirror-vscode-keymap
27
+ editorMode.of({ type: 'vscode' }),
28
+ keymap.of(vscodeKeymap),
29
+ ],
22
30
  vim: [
31
+ // https://github.com/replit/codemirror-vim
23
32
  vim(),
24
33
  editorMode.of({ type: 'vim', noTabster: true }),
25
34
  keymap.of([
@@ -27,7 +36,7 @@ export const EditorModes: { [mode: string]: Extension } = {
27
36
  key: 'Alt-Escape',
28
37
  run: (view) => {
29
38
  // Focus container for tab navigation.
30
- view.dispatch({ userEvent: 'focus.container' });
39
+ view.dispatch({ userEvent: focusEvent });
31
40
  return true;
32
41
  },
33
42
  },
@@ -0,0 +1,90 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type Extension, Transaction } from '@codemirror/state';
6
+ import { EditorView, keymap } from '@codemirror/view';
7
+
8
+ import { debounce } from '@dxos/async';
9
+ import { invariant } from '@dxos/invariant';
10
+ import { isNotFalsy } from '@dxos/util';
11
+
12
+ import { documentId } from './doc';
13
+
14
+ const scrollAnnotation = 'dxos.org/cm/scrolling';
15
+
16
+ // NOTE: Serializable.
17
+ export type SelectionState = {
18
+ scrollTo: {
19
+ from: number;
20
+ };
21
+ selection: {
22
+ anchor: number;
23
+ head?: number;
24
+ };
25
+ };
26
+
27
+ export type StateOptions = {
28
+ setState: (id: string, state: SelectionState) => void;
29
+ getState: (id: string) => SelectionState | undefined;
30
+ };
31
+
32
+ const keyPrefix = 'dxos.org/react-ui-editor/state';
33
+ export const localStorageStateStoreAdapter: StateOptions = {
34
+ setState: (id, state) => {
35
+ invariant(id);
36
+ localStorage.setItem(`${keyPrefix}/${id}`, JSON.stringify(state));
37
+ },
38
+ getState: (id) => {
39
+ invariant(id);
40
+ const state = localStorage.getItem(`${keyPrefix}/${id}`);
41
+ return state ? JSON.parse(state) : undefined;
42
+ },
43
+ };
44
+
45
+ /**
46
+ * Track scrolling and selection state to be restored when switching to document.
47
+ */
48
+ export const state = ({ getState, setState }: Partial<StateOptions> = {}): Extension => {
49
+ const setStateDebounced = debounce(setState!, 1_000);
50
+
51
+ return [
52
+ // TODO(burdon): Track scrolling (currently only updates when cursor moves).
53
+ EditorView.updateListener.of(({ view, changes, transactions }) => {
54
+ // TODO(burdon): Don't react to initial scroll.
55
+ const id = view.state.facet(documentId);
56
+ if (!id || transactions.some((tr) => tr.isUserEvent(scrollAnnotation))) {
57
+ return;
58
+ }
59
+
60
+ if (setState) {
61
+ const { top } = view.dom.getBoundingClientRect();
62
+ const pos = view.posAtCoords({ x: 0, y: top });
63
+ if (pos !== null) {
64
+ const { anchor, head } = view.state.selection.main;
65
+ setStateDebounced(id, {
66
+ scrollTo: { from: pos, yMargin: 0 },
67
+ selection: { anchor, head },
68
+ });
69
+ }
70
+ }
71
+ }),
72
+ getState &&
73
+ keymap.of([
74
+ {
75
+ key: 'ctrl-r', // TODO(burdon): Setting to jump back to bookmark.
76
+ run: (view) => {
77
+ const state = getState(view.state.facet(documentId));
78
+ if (state) {
79
+ view.dispatch({
80
+ effects: EditorView.scrollIntoView(state.scrollTo.from, { yMargin: 0 }),
81
+ selection: state.selection,
82
+ annotations: Transaction.userEvent.of(scrollAnnotation),
83
+ });
84
+ }
85
+ return true;
86
+ },
87
+ },
88
+ ]),
89
+ ].filter(isNotFalsy);
90
+ };
@@ -2,92 +2,11 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import type { EditorView } from '@codemirror/view';
5
+ import { type EditorView } from '@codemirror/view';
6
6
 
7
- import type { ToolbarProps } from '../components';
8
- import {
9
- Inline,
10
- List,
11
- addBlockquote,
12
- addCodeblock,
13
- addLink,
14
- addList,
15
- createComment,
16
- insertTable,
17
- removeBlockquote,
18
- removeCodeblock,
19
- removeLink,
20
- removeList,
21
- setHeading,
22
- setStyle,
23
- toggleBlockquote,
24
- toggleList,
25
- toggleStyle,
26
- } from '../extensions';
7
+ import { type ToolbarProps } from '../components';
8
+ import { processAction } from '../extensions';
27
9
 
28
10
  export const useActionHandler = (view?: EditorView | null): ToolbarProps['onAction'] => {
29
- return (action) => {
30
- if (!view) {
31
- return;
32
- }
33
-
34
- let inlineType, listType;
35
- switch (action.type) {
36
- case 'heading':
37
- setHeading(parseInt(action.data))(view);
38
- break;
39
-
40
- case 'strong':
41
- case 'emphasis':
42
- case 'strikethrough':
43
- case 'code':
44
- inlineType =
45
- action.type === 'strong'
46
- ? Inline.Strong
47
- : action.type === 'emphasis'
48
- ? Inline.Emphasis
49
- : action.type === 'strikethrough'
50
- ? Inline.Strikethrough
51
- : Inline.Code;
52
- (typeof action.data === 'boolean' ? setStyle(inlineType, action.data) : toggleStyle(inlineType))(view);
53
- break;
54
-
55
- case 'list-ordered':
56
- case 'list-bullet':
57
- case 'list-task':
58
- listType =
59
- action.type === 'list-ordered' ? List.Ordered : action.type === 'list-bullet' ? List.Bullet : List.Task;
60
- (action.data === false
61
- ? removeList(listType)
62
- : action.data === true
63
- ? addList(listType)
64
- : toggleList(listType))(view);
65
- break;
66
-
67
- case 'blockquote':
68
- (action.data === false ? removeBlockquote : action.data === true ? addBlockquote : toggleBlockquote)(view);
69
- break;
70
- case 'codeblock':
71
- (action.data === false ? removeCodeblock : addCodeblock)(view);
72
- break;
73
- case 'table':
74
- insertTable(view);
75
- break;
76
-
77
- case 'link':
78
- (action.data === false ? removeLink : addLink)(view);
79
- break;
80
-
81
- case 'comment':
82
- createComment(view);
83
- break;
84
- }
85
-
86
- // TODO(burdon): Hack otherwise remains on heading selector.
87
- setTimeout(() => {
88
- if (!view.hasFocus) {
89
- view.focus();
90
- }
91
- });
92
- };
11
+ return (action) => view && processAction(view, action);
93
12
  };
@@ -4,11 +4,17 @@
4
4
 
5
5
  import { useMemo } from 'react';
6
6
 
7
- import { createDocAccessor, type DocAccessor, getTextContent, type TextObject } from '@dxos/echo-schema';
7
+ import {
8
+ createDocAccessor,
9
+ type DocAccessor,
10
+ getTextContent,
11
+ type TextObject,
12
+ type EchoReactiveObject,
13
+ } from '@dxos/echo-schema';
8
14
 
9
15
  // TODO(burdon): Factor out.
10
16
  export const useDocAccessor = <T = any>(
11
- text: TextObject,
17
+ text: TextObject | EchoReactiveObject<{ content: string }>,
12
18
  ): { id: string; doc: string | undefined; accessor: DocAccessor<T> } => {
13
19
  return useMemo(
14
20
  () => ({
@@ -67,7 +67,7 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
67
67
  <EditorModeToolbar editorMode={editorMode} setEditorMode={setEditorMode} />
68
68
  </Toolbar.Root>
69
69
  <div role='none' className='grow overflow-hidden'>
70
- <div role='textbox' className={mx(textBlockWidth, attentionSurface)} ref={parentRef} />
70
+ <div className={mx(textBlockWidth, attentionSurface)} ref={parentRef} />
71
71
  </div>
72
72
  </div>
73
73
  );
@@ -117,7 +117,7 @@ export default {
117
117
  export const Default = {
118
118
  render: () => {
119
119
  const { parentRef } = useTextEditor();
120
- return <div role='textbox' className={mx(textBlockWidth, attentionSurface)} ref={parentRef} />;
120
+ return <div className={mx(textBlockWidth, attentionSurface)} ref={parentRef} />;
121
121
  },
122
122
  };
123
123
 
@@ -127,7 +127,7 @@ export const Basic = {
127
127
  const { parentRef } = useTextEditor(() => ({
128
128
  extensions: [createBasicExtensions({ placeholder: 'Enter text...' }), createThemeExtensions({ themeMode })],
129
129
  }));
130
- return <div role='textbox' className={mx(textBlockWidth, attentionSurface)} ref={parentRef} />;
130
+ return <div className={mx(textBlockWidth, attentionSurface)} ref={parentRef} />;
131
131
  },
132
132
  };
133
133
 
@@ -33,6 +33,8 @@ export const useTextEditor = (cb: () => UseTextEditorProps = () => ({}), deps: D
33
33
  useEffect(() => {
34
34
  let view: EditorView;
35
35
  if (parentRef.current) {
36
+ log('create', { id });
37
+
36
38
  // https://codemirror.net/docs/ref/#state.EditorStateConfig
37
39
  const state = EditorState.create({
38
40
  doc,
@@ -69,6 +71,7 @@ export const useTextEditor = (cb: () => UseTextEditorProps = () => ({}), deps: D
69
71
  }
70
72
 
71
73
  return () => {
74
+ log('destroy', { id });
72
75
  view?.destroy();
73
76
  };
74
77
  }, deps);
@@ -236,28 +236,44 @@ export const defaultTheme: ThemeStyles = {
236
236
  * </div>
237
237
  * </div
238
238
  */
239
- '.cm-panels': {
240
- border: `1px solid ${get(tokens, 'extend.colors.neutral.200')}`,
241
- },
239
+ '.cm-panels': {},
242
240
  '.cm-panel': {
243
- background: get(tokens, 'extend.colors.neutral.50'),
244
241
  fontFamily: get(tokens, 'fontFamily.body', []).join(','),
245
242
  },
243
+ '.cm-panel input[type=checkbox]': {
244
+ marginRight: '0.4rem !important',
245
+ },
246
+ '&light .cm-panel': {
247
+ background: get(tokens, 'extend.colors.neutral.50'),
248
+ },
249
+ '&dark .cm-panel': {
250
+ background: get(tokens, 'extend.colors.neutral.850'),
251
+ },
246
252
  '.cm-button': {
247
253
  margin: '4px',
248
254
  fontFamily: get(tokens, 'fontFamily.body', []).join(','),
249
- background: get(tokens, 'extend.colors.neutral.100'),
250
255
  backgroundImage: 'none',
251
256
  border: 'none',
257
+ '&:active': {
258
+ backgroundImage: 'none',
259
+ },
260
+ },
261
+ '&light .cm-button': {
262
+ background: get(tokens, 'extend.colors.neutral.100'),
252
263
  '&:hover': {
253
264
  background: get(tokens, 'extend.colors.neutral.200'),
254
265
  },
255
266
  '&:active': {
256
267
  background: get(tokens, 'extend.colors.neutral.300'),
257
- backgroundImage: 'none',
258
268
  },
259
269
  },
260
- '.cm-panel input[type=checkbox]': {
261
- marginRight: '0.4rem !important',
270
+ '&dark .cm-button': {
271
+ background: get(tokens, 'extend.colors.neutral.800'),
272
+ '&:hover': {
273
+ background: get(tokens, 'extend.colors.neutral.700'),
274
+ },
275
+ '&:active': {
276
+ background: get(tokens, 'extend.colors.neutral.600'),
277
+ },
262
278
  },
263
279
  };
@@ -19,6 +19,7 @@ export default [
19
19
  'blockquote label': 'Block quote',
20
20
  'codeblock label': 'Code block',
21
21
  'comment label': 'Create comment',
22
+ 'image label': 'Insert image',
22
23
  'heading label': 'Heading level',
23
24
  'table label': 'Create table',
24
25
  },
@@ -1,7 +0,0 @@
1
- import { type Extension } from '@codemirror/state';
2
- type Options = {
3
- lines?: number;
4
- };
5
- export declare const cursorLineMargin: (options?: Options) => Extension;
6
- export {};
7
- //# sourceMappingURL=cursor-line-margin.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"cursor-line-margin.d.ts","sourceRoot":"","sources":["../../../../src/extensions/cursor-line-margin.ts"],"names":[],"mappings":"AAOA,OAAO,EAAoB,KAAK,SAAS,EAAe,MAAM,mBAAmB,CAAC;AAQlF,KAAK,OAAO,GAAG;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAGF,eAAO,MAAM,gBAAgB,aAAa,OAAO,KAAkB,SAyElE,CAAC"}