@dxos/react-ui-editor 0.3.10-main.ee543cf → 0.3.10-next.3e4f36f

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 (121) hide show
  1. package/dist/lib/browser/index.mjs +735 -731
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/automerge/automerge-plugin/PatchSemaphore.d.ts.map +1 -0
  5. package/dist/types/src/automerge/automerge-plugin/amToCodemirror.d.ts.map +1 -0
  6. package/dist/types/src/automerge/automerge-plugin/codeMirrorToAm.d.ts.map +1 -0
  7. package/dist/types/src/automerge/automerge-plugin/handle.d.ts.map +1 -0
  8. package/dist/types/src/automerge/automerge-plugin/index.d.ts.map +1 -0
  9. package/dist/types/src/automerge/automerge-plugin/plugin.d.ts.map +1 -0
  10. package/dist/types/src/automerge/automerge.stories.d.ts.map +1 -0
  11. package/dist/types/src/components/TextEditor/TextEditor.d.ts +2 -4
  12. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  13. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +4 -9
  14. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  15. package/dist/types/src/components/TextEditor/extensions/{basic.d.ts → basic/bundle.d.ts} +1 -1
  16. package/dist/types/src/components/TextEditor/extensions/basic/bundle.d.ts.map +1 -0
  17. package/dist/types/src/components/TextEditor/extensions/basic/index.d.ts +3 -0
  18. package/dist/types/src/components/TextEditor/extensions/basic/index.d.ts.map +1 -0
  19. package/dist/types/src/components/TextEditor/extensions/basic/theme.d.ts +10 -0
  20. package/dist/types/src/components/TextEditor/extensions/basic/theme.d.ts.map +1 -0
  21. package/dist/types/src/components/TextEditor/extensions/change.d.ts +7 -0
  22. package/dist/types/src/components/TextEditor/extensions/change.d.ts.map +1 -0
  23. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkDecoration.d.ts +11 -0
  24. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkDecoration.d.ts.map +1 -0
  25. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkTooltip.d.ts +8 -0
  26. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkTooltip.d.ts.map +1 -0
  27. package/dist/types/src/components/TextEditor/extensions/{experimental.d.ts → hyperlink/hyperlinkWidget.d.ts} +2 -2
  28. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkWidget.d.ts.map +1 -0
  29. package/dist/types/src/components/TextEditor/extensions/hyperlink/index.d.ts +4 -0
  30. package/dist/types/src/components/TextEditor/extensions/hyperlink/index.d.ts.map +1 -0
  31. package/dist/types/src/components/TextEditor/extensions/index.d.ts +2 -5
  32. package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -1
  33. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +1 -1
  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/markdown/tags.d.ts +17 -0
  37. package/dist/types/src/components/TextEditor/extensions/markdown/tags.d.ts.map +1 -0
  38. package/dist/types/src/components/TextEditor/extensions/markdown/theme.d.ts +21 -0
  39. package/dist/types/src/components/TextEditor/extensions/markdown/theme.d.ts.map +1 -0
  40. package/dist/types/src/components/TextEditor/index.d.ts +0 -2
  41. package/dist/types/src/components/TextEditor/index.d.ts.map +1 -1
  42. package/dist/types/src/components/TextEditor/yjs.stories.d.ts +0 -1
  43. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +1 -1
  44. package/dist/types/src/hooks/index.d.ts +1 -0
  45. package/dist/types/src/hooks/index.d.ts.map +1 -1
  46. package/dist/types/src/hooks/useCollaboration.d.ts +9 -0
  47. package/dist/types/src/hooks/useCollaboration.d.ts.map +1 -0
  48. package/dist/types/src/hooks/useTextModel.d.ts +4 -4
  49. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  50. package/dist/types/src/styles/markdown.d.ts +10 -12
  51. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  52. package/package.json +32 -20
  53. package/src/{hooks/automerge → automerge}/automerge.stories.tsx +1 -2
  54. package/src/components/TextEditor/TextEditor.stories.tsx +43 -46
  55. package/src/components/TextEditor/TextEditor.tsx +45 -59
  56. package/src/components/TextEditor/extensions/{basic.ts → basic/bundle.ts} +2 -4
  57. package/src/components/TextEditor/extensions/basic/index.ts +6 -0
  58. package/src/components/TextEditor/extensions/basic/theme.ts +49 -0
  59. package/src/components/TextEditor/extensions/{listener.ts → change.ts} +3 -3
  60. package/src/components/TextEditor/extensions/{link.ts → hyperlink/hyperlinkDecoration.ts} +31 -68
  61. package/src/components/TextEditor/extensions/{tooltip.tsx → hyperlink/hyperlinkTooltip.tsx} +9 -9
  62. package/src/components/TextEditor/extensions/{experimental.tsx → hyperlink/hyperlinkWidget.tsx} +2 -17
  63. package/src/components/TextEditor/extensions/hyperlink/index.ts +7 -0
  64. package/src/components/TextEditor/extensions/index.ts +2 -5
  65. package/src/components/TextEditor/extensions/markdown/bundle.ts +11 -15
  66. package/src/components/TextEditor/extensions/markdown/index.ts +1 -1
  67. package/src/components/TextEditor/extensions/markdown/tags.ts +38 -0
  68. package/src/components/TextEditor/extensions/markdown/theme.ts +265 -0
  69. package/src/components/TextEditor/index.ts +0 -3
  70. package/src/components/TextEditor/yjs.stories.tsx +0 -1
  71. package/src/hooks/index.ts +1 -0
  72. package/src/hooks/useCollaboration.ts +45 -0
  73. package/src/hooks/useTextModel.ts +40 -50
  74. package/src/styles/markdown.ts +24 -26
  75. package/src/testing/replicator.ts +2 -2
  76. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +0 -35
  77. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +0 -1
  78. package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts +0 -9
  79. package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts.map +0 -1
  80. package/dist/types/src/components/TextEditor/extensions/basic.d.ts.map +0 -1
  81. package/dist/types/src/components/TextEditor/extensions/experimental.d.ts.map +0 -1
  82. package/dist/types/src/components/TextEditor/extensions/link.d.ts +0 -11
  83. package/dist/types/src/components/TextEditor/extensions/link.d.ts.map +0 -1
  84. package/dist/types/src/components/TextEditor/extensions/listener.d.ts +0 -7
  85. package/dist/types/src/components/TextEditor/extensions/listener.d.ts.map +0 -1
  86. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts +0 -31
  87. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +0 -1
  88. package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts +0 -5
  89. package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts.map +0 -1
  90. package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts +0 -12
  91. package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts.map +0 -1
  92. package/dist/types/src/components/TextEditor/themes/default.d.ts +0 -17
  93. package/dist/types/src/components/TextEditor/themes/default.d.ts.map +0 -1
  94. package/dist/types/src/components/TextEditor/themes/index.d.ts +0 -2
  95. package/dist/types/src/components/TextEditor/themes/index.d.ts.map +0 -1
  96. package/dist/types/src/hooks/automerge/PatchSemaphore.d.ts.map +0 -1
  97. package/dist/types/src/hooks/automerge/amToCodemirror.d.ts.map +0 -1
  98. package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +0 -1
  99. package/dist/types/src/hooks/automerge/codeMirrorToAm.d.ts.map +0 -1
  100. package/dist/types/src/hooks/automerge/handle.d.ts.map +0 -1
  101. package/dist/types/src/hooks/automerge/index.d.ts.map +0 -1
  102. package/dist/types/src/hooks/automerge/plugin.d.ts.map +0 -1
  103. package/src/components/TextEditor/MarkdownEditor.stories.tsx +0 -242
  104. package/src/components/TextEditor/extensions/autocomplete.ts +0 -72
  105. package/src/components/TextEditor/extensions/markdown/highlight.ts +0 -166
  106. package/src/components/TextEditor/extensions/tasklist.ts +0 -113
  107. package/src/components/TextEditor/themes/default.ts +0 -170
  108. package/src/components/TextEditor/themes/index.ts +0 -5
  109. /package/dist/types/src/{hooks/automerge → automerge/automerge-plugin}/PatchSemaphore.d.ts +0 -0
  110. /package/dist/types/src/{hooks/automerge → automerge/automerge-plugin}/amToCodemirror.d.ts +0 -0
  111. /package/dist/types/src/{hooks/automerge → automerge/automerge-plugin}/codeMirrorToAm.d.ts +0 -0
  112. /package/dist/types/src/{hooks/automerge → automerge/automerge-plugin}/handle.d.ts +0 -0
  113. /package/dist/types/src/{hooks/automerge → automerge/automerge-plugin}/index.d.ts +0 -0
  114. /package/dist/types/src/{hooks/automerge → automerge/automerge-plugin}/plugin.d.ts +0 -0
  115. /package/dist/types/src/{hooks/automerge → automerge}/automerge.stories.d.ts +0 -0
  116. /package/src/{hooks/automerge → automerge/automerge-plugin}/PatchSemaphore.ts +0 -0
  117. /package/src/{hooks/automerge → automerge/automerge-plugin}/amToCodemirror.ts +0 -0
  118. /package/src/{hooks/automerge → automerge/automerge-plugin}/codeMirrorToAm.ts +0 -0
  119. /package/src/{hooks/automerge → automerge/automerge-plugin}/handle.ts +0 -0
  120. /package/src/{hooks/automerge → automerge/automerge-plugin}/index.ts +0 -0
  121. /package/src/{hooks/automerge → automerge/automerge-plugin}/plugin.ts +0 -0
@@ -2,24 +2,21 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { type Extension } from '@codemirror/state';
6
5
  import { hoverTooltip } from '@codemirror/view';
7
6
 
8
7
  import { tooltipContent } from '@dxos/react-ui-theme';
9
8
 
10
9
  const markdownLinkRegexp = /\[([^\]]+)]\(([^)]+)\)/;
11
10
 
12
- export type TooltipOptions = {
13
- onHover: (el: Element, url: string) => void;
14
- regexp?: RegExp;
15
- };
11
+ export type OnHyperlinkHover = (el: Element, url: string) => void;
16
12
 
17
13
  /**
18
14
  * https://codemirror.net/examples/tooltip
19
15
  * https://codemirror.net/docs/ref/#view.hoverTooltip
20
16
  * https://github.com/codemirror/view/blob/main/src/tooltip.ts
21
17
  */
22
- export const tooltip = ({ onHover, regexp = markdownLinkRegexp }: TooltipOptions): Extension =>
18
+ // TODO(burdon): Create config object.
19
+ export const createHyperlinkTooltip = (onHover: OnHyperlinkHover, regexp = markdownLinkRegexp) =>
23
20
  hoverTooltip((view, pos) => {
24
21
  const { from, text } = view.state.doc.lineAt(pos);
25
22
  const p = pos - from;
@@ -52,10 +49,13 @@ export const tooltip = ({ onHover, regexp = markdownLinkRegexp }: TooltipOptions
52
49
  end: idx + from + match[0].length,
53
50
  above: true,
54
51
  create: () => {
55
- const el = document.createElement('div');
56
- el.className = tooltipContent({}, 'pli-2 plb-1');
52
+ const el = document.createElement('a');
53
+ el.innerText = '_'; // Required so doesn't collapse.
54
+ el.className = tooltipContent({}, 'mb-2 p-1');
55
+ el.setAttribute('target', '_blank');
56
+ el.setAttribute('href', url);
57
57
  onHover(el, url);
58
- return { dom: el, offset: { x: 0, y: 12 } };
58
+ return { dom: el };
59
59
  },
60
60
  };
61
61
  });
@@ -13,27 +13,12 @@ import {
13
13
  type ViewUpdate,
14
14
  } from '@codemirror/view';
15
15
 
16
- // TODO(burdon): Modes: parallel parsing and decoration (e.g., associated with language).
17
- // TODO(burdon): Add-on: runmode: run lexer over content (with rendering codemirror).
18
- // https://codemirror.net/5/doc/manual.html#addon_runmode
19
- // TODO(burdon): Add-on: dialog.
20
- // TODO(burdon): Comments: https://codemirror.net/5/doc/manual.html#setBookmark
21
- // TODO(burdon): Split view: https://codemirror.net/examples/split
22
-
23
- // https://codemirror.net/examples/autocompletion
24
- // https://codemirror.net/docs/ref/#autocomplete.autocompletion
25
- // https://codemirror.net/docs/ref/#autocomplete.Completion
26
-
27
- // TODO(burdon): Hint to customize?
28
- // https://codemirror.net/examples/autocompletion
29
-
30
16
  // Notes: Can't edit widget content (or cursor nav through them).
31
17
  // - https://discuss.codemirror.net/t/focusing-inputs-within-widgets/5178/7
32
18
  // Widgets intentionally always get set to contenteditable=false,
33
19
  // or they would become part of CodeMirror’s editable content element.
34
20
  // You should be able to introduce new contenteditable=true child elements inside of them.
35
21
 
36
- // TODO(burdon): URL match?
37
22
  const markdownLinkRegexp = /\[([^\]]+)]\(([^)]+)\)(!?)/gi;
38
23
 
39
24
  /**
@@ -117,7 +102,7 @@ export function hyperLinkExtension() {
117
102
  );
118
103
  }
119
104
 
120
- export const styles = EditorView.baseTheme({
105
+ export const hyperlinkStyle = EditorView.baseTheme({
121
106
  '.cm-hyperlink-label': {
122
107
  cursor: 'pointer',
123
108
  textDecoration: 'underline',
@@ -131,4 +116,4 @@ export const styles = EditorView.baseTheme({
131
116
  },
132
117
  });
133
118
 
134
- export const hyperlinkWidget: Extension = [hyperLinkExtension(), styles];
119
+ export const hyperlinkWidget: Extension = [hyperLinkExtension(), hyperlinkStyle];
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export * from './hyperlinkDecoration';
6
+ export * from './hyperlinkTooltip';
7
+ export * from './hyperlinkWidget';
@@ -2,10 +2,7 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- export * from './autocomplete';
6
5
  export * from './basic';
7
- export * from './link';
8
- export * from './listener';
6
+ export * from './change';
7
+ export * from './hyperlink';
9
8
  export * from './markdown';
10
- export * from './tasklist';
11
- export * from './tooltip';
@@ -29,10 +29,12 @@ import {
29
29
  placeholder,
30
30
  rectangularSelection,
31
31
  } from '@codemirror/view';
32
+ // import { GFM } from '@lezer/markdown';
32
33
 
33
34
  import type { ThemeMode } from '@dxos/react-ui';
34
35
 
35
- import { markdownHighlightStyle, markdownTagsExtension } from './highlight';
36
+ import { markdownTagsExtension } from './tags';
37
+ import { markdownHighlightStyle } from './theme';
36
38
 
37
39
  export type MarkdownBundleOptions = {
38
40
  themeMode?: ThemeMode;
@@ -75,29 +77,23 @@ export const markdownBundle = ({ themeMode, placeholder: _placeholder }: Markdow
75
77
 
76
78
  // Main extension.
77
79
  // https://github.com/codemirror/lang-markdown
78
- // https://codemirror.net/5/mode/markdown/index.html (demo).
79
80
  markdown({
80
- // GRM by default (vs strict CommonMark):
81
- // Table, TaskList, Strikethrough, and Autolink.
82
- // NOTE: This extends the parser; it doesn't affect rendering.
83
- // https://github.github.com/gfm
84
- // https://github.com/lezer-parser/markdown?tab=readme-ov-file#github-flavored-markdown
85
81
  base: markdownLanguage,
86
-
87
- // Languages for syntax highlighting fenced code blocks.
88
82
  codeLanguages: languages,
89
-
90
- // Parser extensions.
91
83
  extensions: [
92
- // GFM, // TODO(burdon): Provided by default?
84
+ // TODO(burdon): This seems to upgrade the parser.
85
+ // GitHub flavored markdown bundle: Table, TaskList, Strikethrough, and Autolink.
86
+ // https://github.com/lezer-parser/markdown?tab=readme-ov-file#github-flavored-markdown
87
+ // https://github.github.com/gfm
88
+ // GFM,
89
+
90
+ // Custom styling.
93
91
  markdownTagsExtension,
94
92
  ],
95
93
  }),
96
94
 
97
- // Custom styles.
98
- syntaxHighlighting(markdownHighlightStyle),
99
-
100
95
  // TODO(thure): All but one rule here apply to both themes; rename or refactor.
96
+ syntaxHighlighting(markdownHighlightStyle),
101
97
  themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
102
98
  ].filter(Boolean) as Extension[];
103
99
  };
@@ -3,4 +3,4 @@
3
3
  //
4
4
 
5
5
  export * from './bundle';
6
- export * from './highlight';
6
+ export * from './theme';
@@ -0,0 +1,38 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { styleTags, Tag } from '@lezer/highlight';
6
+ import { type MarkdownConfig } from '@lezer/markdown';
7
+
8
+ export const markdownTags = {
9
+ headingMark: Tag.define(),
10
+ quoteMark: Tag.define(),
11
+ listMark: Tag.define(),
12
+ linkMark: Tag.define(),
13
+ emphasisMark: Tag.define(),
14
+ codeMark: Tag.define(),
15
+ codeText: Tag.define(),
16
+ inlineCode: Tag.define(),
17
+ url: Tag.define(),
18
+ linkReference: Tag.define(),
19
+ linkLabel: Tag.define(),
20
+ };
21
+
22
+ export const markdownTagsExtension: MarkdownConfig = {
23
+ props: [
24
+ styleTags({
25
+ HeaderMark: markdownTags.headingMark,
26
+ QuoteMark: markdownTags.quoteMark,
27
+ ListMark: markdownTags.listMark,
28
+ LinkMark: markdownTags.linkMark,
29
+ EmphasisMark: markdownTags.emphasisMark,
30
+ CodeMark: markdownTags.codeMark,
31
+ CodeText: markdownTags.codeText,
32
+ InlineCode: markdownTags.inlineCode,
33
+ URL: markdownTags.url,
34
+ LinkReference: markdownTags.linkReference,
35
+ LinkLabel: markdownTags.linkLabel,
36
+ }),
37
+ ],
38
+ };
@@ -0,0 +1,265 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { markdownLanguage } from '@codemirror/lang-markdown';
6
+ import { HighlightStyle } from '@codemirror/language';
7
+ import { tags } from '@lezer/highlight';
8
+ import get from 'lodash.get';
9
+
10
+ import { markdownTags } from './tags';
11
+ import { bold, heading, italic, mark, strikethrough, tokens, type ThemeStyles } from '../../../../styles';
12
+
13
+ // TODO(burdon): Rationalize theme/colors. Hoist onto single property object.
14
+ export const chalky = '#e5c07b';
15
+ export const coral = '#e06c75';
16
+ export const cyan = '#56b6c2';
17
+ export const invalid = '#ffffff';
18
+ export const ivory = '#abb2bf';
19
+ export const stone = '#7d8799';
20
+ export const malibu = '#61afef';
21
+ export const sage = '#98c379';
22
+ export const whiskey = '#d19a66';
23
+ export const violet = '#c678dd';
24
+ const _darkBackground = '#21252b';
25
+ export const highlightBackground = '#2c313a';
26
+ const _background = '#282c34';
27
+ export const tooltipBackground = '#353a42';
28
+ const _selection = '#3E4451';
29
+
30
+ const monospace = get(tokens, 'fontFamily.mono', ['monospace']).join(',');
31
+
32
+ /**
33
+ * NOTE: The '&' prefix denotes the CM editor root.
34
+ * https://codemirror.net/examples/styling
35
+ */
36
+ export const markdownTheme: ThemeStyles = {
37
+ // TODO(thure): Consider whether these commented-out rules from one-dark-theme should be integrated.
38
+ // '&': {
39
+ // color: ivory,
40
+ // backgroundColor: background
41
+ // },
42
+ // '.cm-cursor, .cm-dropCursor': { borderLeftColor: cursor },
43
+ // '&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {
44
+ // backgroundColor: selection
45
+ // },
46
+ // '.cm-panels': { backgroundColor: darkBackground, color: ivory },
47
+ // '.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
48
+ // '.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
49
+ // '.cm-searchMatch': {
50
+ // backgroundColor: '#72a1ff59',
51
+ // outline: '1px solid #457dff'
52
+ // },
53
+ // '.cm-searchMatch.cm-searchMatch-selected': {
54
+ // backgroundColor: '#6199ff2f'
55
+ // },
56
+ // '.cm-activeLine': { backgroundColor: '#6699ff0b' },
57
+ // '.cm-selectionMatch': { backgroundColor: '#aafe661a' },
58
+ // '&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
59
+ // backgroundColor: '#bad0f847'
60
+ // },
61
+ // '.cm-gutters': {
62
+ // backgroundColor: background,
63
+ // color: stone,
64
+ // border: 'none'
65
+ // },
66
+ // '.cm-activeLineGutter': {
67
+ // backgroundColor: highlightBackground
68
+ // },
69
+ // '.cm-foldPlaceholder': {
70
+ // backgroundColor: 'transparent',
71
+ // border: 'none',
72
+ // color: '#ddd'
73
+ // },
74
+
75
+ '& .cm-tooltip': {
76
+ backgroundColor: 'transparent',
77
+ border: 'none',
78
+ },
79
+ // '.dark & .cm-tooltip': {
80
+ // border: 'none',
81
+ // backgroundColor: tooltipBackground,
82
+ // },
83
+ // '.dark & .cm-tooltip .cm-tooltip-arrow:before': {
84
+ // borderTopColor: 'transparent',
85
+ // borderBottomColor: 'transparent',
86
+ // },
87
+ // '.dark & .cm-tooltip .cm-tooltip-arrow:after': {
88
+ // borderTopColor: tooltipBackground,
89
+ // borderBottomColor: tooltipBackground,
90
+ // },
91
+ // '.dark & .cm-tooltip-autocomplete': {
92
+ // '& > ul > li[aria-selected]': {
93
+ // backgroundColor: highlightBackground,
94
+ // color: ivory,
95
+ // },
96
+ // },
97
+ '&.cm-focused': {
98
+ outline: 'none',
99
+ },
100
+ '& .cm-line': {
101
+ paddingInline: 0,
102
+ lineHeight: 1.6,
103
+ minBlockSize: '1.6em',
104
+ },
105
+ '& .cm-line *': {
106
+ lineHeight: 1.6,
107
+ },
108
+ '&.cm-focused .cm-selectionBackground, & .cm-selectionBackground': {
109
+ background: get(tokens, 'extend.colors.primary.150', '#00ffff'),
110
+ },
111
+ '.dark & .cm-selectionBackground, .dark &.cm-focused .cm-selectionBackground': {
112
+ background: get(tokens, 'extend.colors.primary.850', '#00ffff'),
113
+ },
114
+ '& .cm-selectionMatch': {
115
+ background: get(tokens, 'extend.colors.primary.250', '#00ffff') + '44',
116
+ },
117
+ '.dark & .cm-selectionMatch': {
118
+ background: get(tokens, 'extend.colors.primary.600', '#00ffff') + '44',
119
+ },
120
+ '& .cm-content': {
121
+ padding: 0,
122
+ caretColor: 'black',
123
+ },
124
+ '.dark & .cm-content': {
125
+ caretColor: 'white',
126
+ },
127
+ '& .cm-cursor': {
128
+ borderLeftColor: 'black',
129
+ },
130
+ '.dark & .cm-cursor': {
131
+ borderLeftColor: 'white',
132
+ },
133
+ '.cm-placeholder': {
134
+ fontFamily: get(tokens, 'fontFamily.body', []).join(','),
135
+ },
136
+ '& .cm-scroller': {
137
+ fontFamily: get(tokens, 'fontFamily.mono', []).join(','),
138
+ overflow: 'visible',
139
+ },
140
+ '& .cm-activeLine': {
141
+ backgroundColor: 'transparent',
142
+ },
143
+ '.dark & .cm-activeLine': {
144
+ backgroundColor: 'transparent',
145
+ },
146
+ '& .cm-ySelectionInfo': {
147
+ fontFamily: get(tokens, 'fontFamily.body', []).join(','),
148
+ padding: '2px 4px',
149
+ marginBlockStart: '-4px',
150
+ },
151
+ '& .cm-ySelection, & .cm-selectionMatch': {
152
+ paddingBlockStart: '.15em',
153
+ paddingBlockEnd: '.15em',
154
+ },
155
+ '& .cm-ySelection, & .cm-yLineSelection': {
156
+ mixBlendMode: 'multiply',
157
+ },
158
+ '.dark & .cm-ySelection, .dark & .cm-yLineSelection': {
159
+ mixBlendMode: 'screen',
160
+ },
161
+ '& .cm-ySelectionCaret': {
162
+ display: 'inline-block',
163
+ insetBlockStart: '.1em',
164
+ blockSize: '1.4em',
165
+ verticalAlign: 'top',
166
+ },
167
+ '& .cm-yLineSelection': {
168
+ margin: '0',
169
+ },
170
+ '.cm-link': {
171
+ color: 'rgb(20 89 208)', // TODO(burdon): Rationalize.
172
+ textDecorationLine: 'underline',
173
+ textDecorationThickness: '1px',
174
+ textUnderlineOffset: '2px',
175
+ borderRadius: '.125rem',
176
+ },
177
+ // TODO(burdon): Document.
178
+ ...Object.keys(get(tokens, 'extend.fontSize', {})).reduce((acc: Record<string, any>, fontSize) => {
179
+ const height = get(tokens, ['extend', 'fontSize', fontSize, 1, 'lineHeight']);
180
+ // TODO(thure): This appears to be the best or only way to set selection caret heights, but it's far more verbose than it needs to be.
181
+ acc[`& .text-${fontSize} + .cm-ySelectionCaret`] = { height };
182
+ acc[`& .text-${fontSize} + .cm-ySelection + .cm-ySelectionCaret`] = { height };
183
+ acc[`& .text-${fontSize} + .cm-widgetBuffer + .cm-ySelectionCaret`] = { height };
184
+ return acc;
185
+ }, {}),
186
+ };
187
+
188
+ export const markdownHighlightStyle = HighlightStyle.define(
189
+ [
190
+ {
191
+ tag: [
192
+ tags.keyword,
193
+ tags.name,
194
+ tags.deleted,
195
+ tags.character,
196
+ tags.propertyName,
197
+ tags.macroName,
198
+ tags.color,
199
+ tags.constant(tags.name),
200
+ tags.standard(tags.name),
201
+ tags.definition(tags.name),
202
+ tags.separator,
203
+ tags.typeName,
204
+ tags.className,
205
+ tags.number,
206
+ tags.changed,
207
+ tags.annotation,
208
+ tags.modifier,
209
+ tags.self,
210
+ tags.namespace,
211
+ tags.operator,
212
+ tags.operatorKeyword,
213
+ tags.escape,
214
+ tags.regexp,
215
+ tags.special(tags.string),
216
+ tags.meta,
217
+ tags.comment,
218
+ tags.atom,
219
+ tags.bool,
220
+ tags.special(tags.variableName),
221
+ tags.processingInstruction,
222
+ tags.string,
223
+ tags.inserted,
224
+ tags.invalid,
225
+ ],
226
+ color: 'inherit !important',
227
+ },
228
+ {
229
+ tag: [tags.link, tags.url],
230
+ color: 'inherit !important',
231
+ textDecoration: 'none !important',
232
+ },
233
+ {
234
+ tag: [tags.function(tags.variableName), tags.labelName],
235
+ color: malibu,
236
+ fontFamily: monospace,
237
+ },
238
+ {
239
+ tag: [
240
+ markdownTags.codeMark,
241
+ markdownTags.emphasisMark,
242
+ markdownTags.headingMark,
243
+ markdownTags.linkLabel,
244
+ markdownTags.linkReference,
245
+ markdownTags.listMark,
246
+ markdownTags.quoteMark,
247
+ markdownTags.url,
248
+ tags.meta,
249
+ tags.processingInstruction,
250
+ ],
251
+ class: mark,
252
+ },
253
+ { tag: [markdownTags.codeText, markdownTags.inlineCode], class: 'font-mono' },
254
+ { tag: tags.emphasis, class: italic },
255
+ { tag: tags.heading1, class: heading[1] },
256
+ { tag: tags.heading2, class: heading[2] },
257
+ { tag: tags.heading3, class: heading[3] },
258
+ { tag: tags.heading4, class: heading[4] },
259
+ { tag: tags.heading5, class: heading[5] },
260
+ { tag: tags.heading6, class: heading[6] },
261
+ { tag: tags.strikethrough, class: strikethrough },
262
+ { tag: tags.strong, class: bold },
263
+ ],
264
+ { scope: markdownLanguage, all: { fontFamily: get(tokens, 'fontFamily.body', []).join(',') } },
265
+ );
@@ -3,9 +3,6 @@
3
3
  //
4
4
 
5
5
  export { tags } from '@lezer/highlight';
6
- export { type Extension } from '@codemirror/state';
7
6
 
8
7
  export * from './extensions';
9
- export * from './themes';
10
-
11
8
  export * from './TextEditor';
@@ -50,7 +50,6 @@ const Story = () => {
50
50
  };
51
51
 
52
52
  export const Default = {
53
- title: 'react-ui-editor/YJS',
54
53
  // TODO(wittjosiah): Decorator for doing this without clients being initialized?
55
54
  render: () => <ClientRepeater count={2} Component={Story} />,
56
55
  };
@@ -2,5 +2,6 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
+ export * from './useCollaboration';
5
6
  export * from './useTextModel';
6
7
  export * from './yjs';
@@ -0,0 +1,45 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import type { Extension } from '@codemirror/state';
6
+ import { useEffect, useMemo } from 'react';
7
+ import { yCollab } from 'y-codemirror.next';
8
+
9
+ import { generateName } from '@dxos/display-name';
10
+ import { isDocAccessor } from '@dxos/echo-schema';
11
+ import type { ThemeMode } from '@dxos/react-ui';
12
+ import { getColorForValue } from '@dxos/react-ui-theme';
13
+ import { YText } from '@dxos/text-model';
14
+
15
+ import type { EditorModel } from './useTextModel';
16
+ import { automergePlugin } from '../automerge/automerge-plugin';
17
+
18
+ /**
19
+ * Replication and awareness (incl. remote selection).
20
+ * https://codemirror.net/docs/ref/#collab
21
+ */
22
+ export const useCollaboration = (model: EditorModel, themeMode: ThemeMode): Extension | undefined => {
23
+ const { provider, peer, content } = model;
24
+ const extension = useMemo(() => {
25
+ if (content instanceof YText) {
26
+ return yCollab(content, provider?.awareness);
27
+ } else if (isDocAccessor(content)) {
28
+ return automergePlugin(content.handle, content.path);
29
+ } else {
30
+ return undefined;
31
+ }
32
+ }, []);
33
+
34
+ useEffect(() => {
35
+ if (provider && peer) {
36
+ provider.awareness.setLocalStateField('user', {
37
+ name: peer.name ?? generateName(peer.id),
38
+ color: getColorForValue({ value: peer.id, type: 'color' }),
39
+ colorLight: getColorForValue({ value: peer.id, themeMode, type: 'highlight' }),
40
+ });
41
+ }
42
+ }, [provider, peer, themeMode]);
43
+
44
+ return extension;
45
+ };
@@ -2,13 +2,9 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { type Extension } from '@codemirror/state';
6
- import get from 'lodash.get';
7
- import { useEffect, useState } from 'react';
8
- import { yCollab } from 'y-codemirror.next';
5
+ import { useMemo } from 'react';
9
6
  import type * as awarenessProtocol from 'y-protocols/awareness';
10
7
 
11
- import { invariant } from '@dxos/invariant';
12
8
  import {
13
9
  type DocAccessor,
14
10
  type Space,
@@ -20,83 +16,77 @@ import {
20
16
  import { type Identity } from '@dxos/react-client/halo';
21
17
  import type { YText, YXmlFragment } from '@dxos/text-model';
22
18
 
23
- import { automergePlugin } from './automerge';
24
19
  import { SpaceAwarenessProvider } from './yjs';
25
20
 
26
21
  // TODO(burdon): Move.
27
22
  type Awareness = awarenessProtocol.Awareness;
23
+ type Provider = { awareness: Awareness };
28
24
 
29
25
  // TODO(wittjosiah): Factor out to common package? @dxos/react-client?
30
26
  export type EditorModel = {
31
27
  id: string;
32
28
  content: string | YText | YXmlFragment | DocAccessor;
33
- text: () => string;
34
- extension?: Extension;
35
- awareness?: Awareness;
29
+ provider?: Provider;
36
30
  peer?: {
37
31
  id: string;
38
32
  name?: string;
39
33
  };
40
34
  };
41
35
 
42
- // TODO(burdon): Remove space/identity dependency. Define interface for the framework re content and presence.
43
36
  export type UseTextModelOptions = {
44
37
  identity?: Identity | null;
45
38
  space?: Space;
46
39
  text?: TextObject;
47
40
  };
48
41
 
49
- // TODO(burdon): Remove YJS/Automerge deps (from UI component -- create abstraction; incl. all ECHO/Space deps).
42
+ // TODO(burdon): Don't support returning undefined (and remove checks from calling code).
43
+ // TODO(burdon): Decouple space (make Editor less dependent on entire stack)?
50
44
  // TODO(wittjosiah): Factor out to common package? @dxos/react-client?
51
45
  export const useTextModel = ({ identity, space, text }: UseTextModelOptions): EditorModel | undefined => {
52
- const [model, setModel] = useState<EditorModel | undefined>(() => createModel({ identity, space, text }));
53
- useEffect(() => {
54
- setModel(createModel({ identity, space, text }));
55
- }, [identity, space, text]);
56
- return model;
57
- };
58
-
59
- const createModel = (options: UseTextModelOptions) => {
60
- const { space, text } = options;
46
+ const provider = useMemo(() => {
47
+ if (isActualAutomergeObject(text)) {
48
+ return undefined;
49
+ }
61
50
 
62
- if (isActualAutomergeObject(text)) {
63
- return createAutomergeModel(options);
64
- } else {
65
- if (!space || !text?.doc || !text?.content) {
51
+ if (!space || !text?.doc) {
66
52
  return undefined;
67
53
  }
68
- return createYjsModel(options);
54
+
55
+ return new SpaceAwarenessProvider({ space, doc: text.doc, channel: `yjs.awareness.${text.id}` });
56
+ }, [identity, space, text, text?.doc]);
57
+
58
+ const content = useMemo(() => {
59
+ if (isActualAutomergeObject(text)) {
60
+ const obj = text as any as AutomergeTextCompat;
61
+ return getRawDoc(obj, [obj.field]);
62
+ } else {
63
+ return text?.content;
64
+ }
65
+ }, [text]);
66
+
67
+ if (isActualAutomergeObject(text)) {
68
+ const obj = text as any as AutomergeTextCompat;
69
+ return {
70
+ id: obj.id,
71
+ content: content!,
72
+ provider: undefined,
73
+ peer: identity
74
+ ? {
75
+ id: identity.identityKey.toHex(),
76
+ name: identity.profile?.displayName,
77
+ }
78
+ : undefined,
79
+ };
69
80
  }
70
- };
71
81
 
72
- const createYjsModel = ({ identity, space, text }: UseTextModelOptions): EditorModel => {
73
- invariant(space && text?.doc && text?.content);
74
- const provider = new SpaceAwarenessProvider({ space, doc: text.doc, channel: `yjs.awareness.${text.id}` });
82
+ if (!text?.doc || !text?.content) {
83
+ return undefined;
84
+ }
75
85
 
76
86
  return {
77
87
  id: text.doc.guid,
78
88
  content: text.content,
79
- text: () => text.content!.toString(),
80
- extension: yCollab(text.content as YText, provider.awareness),
81
- awareness: provider.awareness,
82
- peer: identity
83
- ? {
84
- id: identity.identityKey.toHex(),
85
- name: identity.profile?.displayName,
86
- }
87
- : undefined,
88
- };
89
- };
90
-
91
- const createAutomergeModel = ({ identity, space, text }: UseTextModelOptions): EditorModel => {
92
- const obj = text as any as AutomergeTextCompat;
93
- const doc = getRawDoc(obj, [obj.field]);
94
-
95
- return {
96
- id: obj.id,
97
- content: doc,
98
- text: () => get(doc.handle.docSync(), doc.path),
99
- extension: automergePlugin(doc.handle, doc.path),
89
+ provider,
100
90
  peer: identity
101
91
  ? {
102
92
  id: identity.identityKey.toHex(),