@dxos/react-ui-editor 0.8.4-main.67995b8 → 0.8.4-main.a4bbb77

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 (261) hide show
  1. package/dist/lib/browser/index.mjs +3067 -1955
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/browser/testing/index.mjs +71 -1
  5. package/dist/lib/browser/testing/index.mjs.map +4 -4
  6. package/dist/lib/node-esm/index.mjs +3067 -1955
  7. package/dist/lib/node-esm/index.mjs.map +4 -4
  8. package/dist/lib/node-esm/meta.json +1 -1
  9. package/dist/lib/node-esm/testing/index.mjs +71 -1
  10. package/dist/lib/node-esm/testing/index.mjs.map +4 -4
  11. package/dist/types/src/components/{Popover → CommandMenu}/CommandMenu.d.ts +10 -6
  12. package/dist/types/src/components/CommandMenu/CommandMenu.d.ts.map +1 -0
  13. package/dist/types/src/components/CommandMenu/index.d.ts +2 -0
  14. package/dist/types/src/components/CommandMenu/index.d.ts.map +1 -0
  15. package/dist/types/src/components/Editor/Editor.d.ts +15 -9
  16. package/dist/types/src/components/Editor/Editor.d.ts.map +1 -1
  17. package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts.map +1 -1
  18. package/dist/types/src/components/EditorToolbar/blocks.d.ts.map +1 -1
  19. package/dist/types/src/components/EditorToolbar/formatting.d.ts.map +1 -1
  20. package/dist/types/src/components/EditorToolbar/headings.d.ts.map +1 -1
  21. package/dist/types/src/components/EditorToolbar/image.d.ts.map +1 -1
  22. package/dist/types/src/components/EditorToolbar/lists.d.ts.map +1 -1
  23. package/dist/types/src/components/EditorToolbar/search.d.ts.map +1 -1
  24. package/dist/types/src/components/EditorToolbar/util.d.ts +5 -5
  25. package/dist/types/src/components/EditorToolbar/util.d.ts.map +1 -1
  26. package/dist/types/src/components/EditorToolbar/view-mode.d.ts +1 -1
  27. package/dist/types/src/components/EditorToolbar/view-mode.d.ts.map +1 -1
  28. package/dist/types/src/components/index.d.ts +1 -1
  29. package/dist/types/src/components/index.d.ts.map +1 -1
  30. package/dist/types/src/defaults.d.ts.map +1 -1
  31. package/dist/types/src/extensions/autocomplete.d.ts +20 -7
  32. package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
  33. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  34. package/dist/types/src/extensions/automerge/automerge.stories.d.ts +10 -19
  35. package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
  36. package/dist/types/src/extensions/automerge/defs.d.ts +1 -1
  37. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
  38. package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
  39. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
  40. package/dist/types/src/extensions/autoscroll.d.ts +10 -0
  41. package/dist/types/src/extensions/autoscroll.d.ts.map +1 -0
  42. package/dist/types/src/extensions/{command → command-dialog}/action.d.ts +1 -1
  43. package/dist/types/src/extensions/command-dialog/action.d.ts.map +1 -0
  44. package/dist/types/src/extensions/{command/command.d.ts → command-dialog/command-dialog.d.ts} +2 -2
  45. package/dist/types/src/extensions/command-dialog/command-dialog.d.ts.map +1 -0
  46. package/dist/types/src/extensions/{command → command-dialog}/hint.d.ts +2 -7
  47. package/dist/types/src/extensions/command-dialog/hint.d.ts.map +1 -0
  48. package/dist/types/src/extensions/command-dialog/index.d.ts +4 -0
  49. package/dist/types/src/extensions/command-dialog/index.d.ts.map +1 -0
  50. package/dist/types/src/extensions/{command → command-dialog}/state.d.ts +1 -1
  51. package/dist/types/src/extensions/command-dialog/state.d.ts.map +1 -0
  52. package/dist/types/src/extensions/command-dialog/typeahead.d.ts.map +1 -0
  53. package/dist/types/src/extensions/{command → command-menu}/command-menu.d.ts +3 -3
  54. package/dist/types/src/extensions/command-menu/command-menu.d.ts.map +1 -0
  55. package/dist/types/src/extensions/command-menu/index.d.ts +3 -0
  56. package/dist/types/src/extensions/command-menu/index.d.ts.map +1 -0
  57. package/dist/types/src/extensions/command-menu/placeholder.d.ts.map +1 -0
  58. package/dist/types/src/extensions/command-menu/useCommandMenu.d.ts +24 -0
  59. package/dist/types/src/extensions/command-menu/useCommandMenu.d.ts.map +1 -0
  60. package/dist/types/src/extensions/comments.d.ts +1 -1
  61. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  62. package/dist/types/src/extensions/dnd.d.ts.map +1 -1
  63. package/dist/types/src/extensions/factories.d.ts +3 -8
  64. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  65. package/dist/types/src/extensions/floating-menu.d.ts.map +1 -0
  66. package/dist/types/src/extensions/focus.d.ts.map +1 -1
  67. package/dist/types/src/extensions/folding.d.ts.map +1 -1
  68. package/dist/types/src/extensions/index.d.ts +5 -1
  69. package/dist/types/src/extensions/index.d.ts.map +1 -1
  70. package/dist/types/src/extensions/markdown/action.d.ts.map +1 -1
  71. package/dist/types/src/extensions/markdown/bundle.d.ts +8 -2
  72. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  73. package/dist/types/src/extensions/markdown/changes.d.ts +1 -1
  74. package/dist/types/src/extensions/markdown/changes.d.ts.map +1 -1
  75. package/dist/types/src/extensions/markdown/decorate.d.ts +9 -1
  76. package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
  77. package/dist/types/src/extensions/markdown/formatting.d.ts +1 -1
  78. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
  79. package/dist/types/src/extensions/markdown/formatting.test.d.ts.map +1 -1
  80. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
  81. package/dist/types/src/extensions/markdown/image.d.ts.map +1 -1
  82. package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
  83. package/dist/types/src/extensions/outliner/outliner.d.ts +1 -1
  84. package/dist/types/src/extensions/outliner/outliner.d.ts.map +1 -1
  85. package/dist/types/src/extensions/outliner/selection.d.ts.map +1 -1
  86. package/dist/types/src/extensions/outliner/tree.d.ts +2 -2
  87. package/dist/types/src/extensions/outliner/tree.d.ts.map +1 -1
  88. package/dist/types/src/extensions/preview/preview.d.ts +3 -6
  89. package/dist/types/src/extensions/preview/preview.d.ts.map +1 -1
  90. package/dist/types/src/extensions/tags/extended-markdown.d.ts +10 -0
  91. package/dist/types/src/extensions/tags/extended-markdown.d.ts.map +1 -0
  92. package/dist/types/src/extensions/tags/extended-markdown.test.d.ts +2 -0
  93. package/dist/types/src/extensions/tags/extended-markdown.test.d.ts.map +1 -0
  94. package/dist/types/src/extensions/tags/index.d.ts +4 -0
  95. package/dist/types/src/extensions/tags/index.d.ts.map +1 -0
  96. package/dist/types/src/extensions/tags/streamer.d.ts +12 -0
  97. package/dist/types/src/extensions/tags/streamer.d.ts.map +1 -0
  98. package/dist/types/src/extensions/tags/xml-tags.d.ts +72 -0
  99. package/dist/types/src/extensions/tags/xml-tags.d.ts.map +1 -0
  100. package/dist/types/src/extensions/tags/xml-util.d.ts +10 -0
  101. package/dist/types/src/extensions/tags/xml-util.d.ts.map +1 -0
  102. package/dist/types/src/hooks/useTextEditor.d.ts +2 -2
  103. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  104. package/dist/types/src/stories/CommandDialog.stories.d.ts +14 -0
  105. package/dist/types/src/stories/CommandDialog.stories.d.ts.map +1 -0
  106. package/dist/types/src/stories/CommandMenu.stories.d.ts +10 -4
  107. package/dist/types/src/stories/CommandMenu.stories.d.ts.map +1 -1
  108. package/dist/types/src/stories/Comments.stories.d.ts +21 -10
  109. package/dist/types/src/stories/Comments.stories.d.ts.map +1 -1
  110. package/dist/types/src/stories/EditorToolbar.stories.d.ts +39 -3
  111. package/dist/types/src/stories/EditorToolbar.stories.d.ts.map +1 -1
  112. package/dist/types/src/stories/Experimental.stories.d.ts +22 -13
  113. package/dist/types/src/stories/Experimental.stories.d.ts.map +1 -1
  114. package/dist/types/src/stories/Markdown.stories.d.ts +32 -43
  115. package/dist/types/src/stories/Markdown.stories.d.ts.map +1 -1
  116. package/dist/types/src/stories/Outliner.stories.d.ts +15 -21
  117. package/dist/types/src/stories/Outliner.stories.d.ts.map +1 -1
  118. package/dist/types/src/stories/Preview.stories.d.ts +21 -7
  119. package/dist/types/src/stories/Preview.stories.d.ts.map +1 -1
  120. package/dist/types/src/stories/Tags.stories.d.ts +16 -0
  121. package/dist/types/src/stories/Tags.stories.d.ts.map +1 -0
  122. package/dist/types/src/stories/TextEditor.stories.d.ts +38 -52
  123. package/dist/types/src/stories/TextEditor.stories.d.ts.map +1 -1
  124. package/dist/types/src/stories/components/EditorStory.d.ts +5 -8
  125. package/dist/types/src/stories/components/EditorStory.d.ts.map +1 -1
  126. package/dist/types/src/styles/theme.d.ts.map +1 -1
  127. package/dist/types/src/testing/PreviewPopover.d.ts +20 -0
  128. package/dist/types/src/testing/PreviewPopover.d.ts.map +1 -0
  129. package/dist/types/src/testing/index.d.ts +1 -0
  130. package/dist/types/src/testing/index.d.ts.map +1 -1
  131. package/dist/types/src/testing/util.d.ts +1 -0
  132. package/dist/types/src/testing/util.d.ts.map +1 -1
  133. package/dist/types/src/translations.d.ts +1 -1
  134. package/dist/types/src/types/types.d.ts +1 -1
  135. package/dist/types/src/util/cursor.d.ts.map +1 -1
  136. package/dist/types/src/util/debug.d.ts +1 -1
  137. package/dist/types/src/util/debug.d.ts.map +1 -1
  138. package/dist/types/src/util/decorations.d.ts +4 -0
  139. package/dist/types/src/util/decorations.d.ts.map +1 -0
  140. package/dist/types/src/util/dom.d.ts +2 -12
  141. package/dist/types/src/util/dom.d.ts.map +1 -1
  142. package/dist/types/src/util/index.d.ts +1 -0
  143. package/dist/types/src/util/index.d.ts.map +1 -1
  144. package/dist/types/src/util/react.d.ts +1 -1
  145. package/dist/types/src/util/react.d.ts.map +1 -1
  146. package/dist/types/tsconfig.tsbuildinfo +1 -1
  147. package/package.json +70 -66
  148. package/src/components/{Popover → CommandMenu}/CommandMenu.tsx +95 -26
  149. package/src/components/{Popover → CommandMenu}/index.ts +0 -2
  150. package/src/components/Editor/Editor.tsx +50 -15
  151. package/src/components/EditorToolbar/EditorToolbar.tsx +40 -30
  152. package/src/components/EditorToolbar/blocks.ts +21 -24
  153. package/src/components/EditorToolbar/formatting.ts +22 -25
  154. package/src/components/EditorToolbar/headings.ts +10 -5
  155. package/src/components/EditorToolbar/image.ts +8 -4
  156. package/src/components/EditorToolbar/lists.ts +16 -19
  157. package/src/components/EditorToolbar/search.ts +8 -4
  158. package/src/components/EditorToolbar/util.ts +16 -5
  159. package/src/components/EditorToolbar/view-mode.ts +11 -6
  160. package/src/components/index.ts +1 -1
  161. package/src/defaults.ts +5 -2
  162. package/src/extensions/autocomplete.ts +204 -54
  163. package/src/extensions/automerge/automerge.stories.tsx +31 -24
  164. package/src/extensions/automerge/automerge.ts +4 -3
  165. package/src/extensions/automerge/defs.ts +1 -1
  166. package/src/extensions/automerge/sync.ts +1 -1
  167. package/src/extensions/automerge/update-automerge.ts +1 -1
  168. package/src/extensions/autoscroll.ts +157 -0
  169. package/src/extensions/awareness/awareness.ts +2 -2
  170. package/src/extensions/{command → command-dialog}/action.ts +2 -3
  171. package/src/extensions/{command/command.ts → command-dialog/command-dialog.ts} +4 -4
  172. package/src/extensions/{command → command-dialog}/hint.ts +2 -1
  173. package/src/extensions/command-dialog/index.ts +7 -0
  174. package/src/extensions/{command → command-dialog}/state.ts +4 -3
  175. package/src/extensions/{command → command-dialog}/typeahead.ts +2 -2
  176. package/src/extensions/{command → command-menu}/command-menu.ts +9 -9
  177. package/src/extensions/command-menu/index.ts +6 -0
  178. package/src/extensions/{command → command-menu}/placeholder.ts +1 -1
  179. package/src/extensions/{command → command-menu}/useCommandMenu.ts +35 -19
  180. package/src/extensions/comments.ts +18 -13
  181. package/src/extensions/dnd.ts +1 -1
  182. package/src/extensions/factories.ts +37 -27
  183. package/src/extensions/{command/floating-menu.ts → floating-menu.ts} +12 -19
  184. package/src/extensions/focus.ts +5 -4
  185. package/src/extensions/folding.tsx +4 -6
  186. package/src/extensions/index.ts +5 -1
  187. package/src/extensions/markdown/action.ts +2 -1
  188. package/src/extensions/markdown/bundle.ts +27 -5
  189. package/src/extensions/markdown/changes.ts +1 -1
  190. package/src/extensions/markdown/decorate.ts +24 -14
  191. package/src/extensions/markdown/formatting.test.ts +6 -6
  192. package/src/extensions/markdown/formatting.ts +3 -3
  193. package/src/extensions/markdown/highlight.ts +1 -1
  194. package/src/extensions/markdown/image.ts +3 -4
  195. package/src/extensions/markdown/link.ts +3 -0
  196. package/src/extensions/markdown/table.ts +7 -1
  197. package/src/extensions/mention.ts +1 -1
  198. package/src/extensions/outliner/outliner.test.ts +3 -2
  199. package/src/extensions/outliner/outliner.ts +6 -5
  200. package/src/extensions/outliner/selection.ts +1 -1
  201. package/src/extensions/outliner/tree.test.ts +2 -1
  202. package/src/extensions/outliner/tree.ts +2 -2
  203. package/src/extensions/preview/preview.ts +59 -62
  204. package/src/extensions/selection.ts +2 -2
  205. package/src/extensions/tags/extended-markdown.test.ts +261 -0
  206. package/src/extensions/tags/extended-markdown.ts +78 -0
  207. package/src/extensions/tags/index.ts +7 -0
  208. package/src/extensions/tags/streamer.ts +243 -0
  209. package/src/extensions/tags/xml-tags.ts +393 -0
  210. package/src/extensions/tags/xml-util.ts +94 -0
  211. package/src/hooks/useTextEditor.ts +8 -20
  212. package/src/stories/CommandDialog.stories.tsx +89 -0
  213. package/src/stories/CommandMenu.stories.tsx +33 -34
  214. package/src/stories/Comments.stories.tsx +14 -10
  215. package/src/stories/EditorToolbar.stories.tsx +13 -12
  216. package/src/stories/Experimental.stories.tsx +17 -13
  217. package/src/stories/Markdown.stories.tsx +25 -21
  218. package/src/stories/Outliner.stories.tsx +46 -34
  219. package/src/stories/Preview.stories.tsx +34 -33
  220. package/src/stories/Tags.stories.tsx +81 -0
  221. package/src/stories/TextEditor.stories.tsx +45 -38
  222. package/src/stories/components/EditorStory.tsx +12 -13
  223. package/src/styles/theme.ts +15 -12
  224. package/src/testing/PreviewPopover.tsx +78 -0
  225. package/src/testing/index.ts +1 -0
  226. package/src/testing/util.ts +2 -0
  227. package/src/translations.ts +1 -1
  228. package/src/util/cursor.ts +2 -1
  229. package/src/util/debug.ts +2 -2
  230. package/src/util/decorations.ts +21 -0
  231. package/src/util/dom.ts +5 -27
  232. package/src/util/index.ts +1 -0
  233. package/src/util/react.tsx +1 -1
  234. package/dist/types/src/components/Popover/CommandMenu.d.ts.map +0 -1
  235. package/dist/types/src/components/Popover/RefDropdownMenu.d.ts +0 -21
  236. package/dist/types/src/components/Popover/RefDropdownMenu.d.ts.map +0 -1
  237. package/dist/types/src/components/Popover/RefPopover.d.ts +0 -34
  238. package/dist/types/src/components/Popover/RefPopover.d.ts.map +0 -1
  239. package/dist/types/src/components/Popover/index.d.ts +0 -4
  240. package/dist/types/src/components/Popover/index.d.ts.map +0 -1
  241. package/dist/types/src/extensions/command/action.d.ts.map +0 -1
  242. package/dist/types/src/extensions/command/command-menu.d.ts.map +0 -1
  243. package/dist/types/src/extensions/command/command.d.ts.map +0 -1
  244. package/dist/types/src/extensions/command/floating-menu.d.ts.map +0 -1
  245. package/dist/types/src/extensions/command/hint.d.ts.map +0 -1
  246. package/dist/types/src/extensions/command/index.d.ts +0 -7
  247. package/dist/types/src/extensions/command/index.d.ts.map +0 -1
  248. package/dist/types/src/extensions/command/placeholder.d.ts.map +0 -1
  249. package/dist/types/src/extensions/command/state.d.ts.map +0 -1
  250. package/dist/types/src/extensions/command/typeahead.d.ts.map +0 -1
  251. package/dist/types/src/extensions/command/useCommandMenu.d.ts +0 -26
  252. package/dist/types/src/extensions/command/useCommandMenu.d.ts.map +0 -1
  253. package/dist/types/src/stories/Command.stories.d.ts +0 -7
  254. package/dist/types/src/stories/Command.stories.d.ts.map +0 -1
  255. package/src/components/Popover/RefDropdownMenu.tsx +0 -85
  256. package/src/components/Popover/RefPopover.tsx +0 -99
  257. package/src/extensions/command/index.ts +0 -10
  258. package/src/stories/Command.stories.tsx +0 -97
  259. /package/dist/types/src/extensions/{command → command-dialog}/typeahead.d.ts +0 -0
  260. /package/dist/types/src/extensions/{command → command-menu}/placeholder.d.ts +0 -0
  261. /package/dist/types/src/extensions/{command/floating-menu.d.ts → floating-menu.d.ts} +0 -0
@@ -5,15 +5,16 @@
5
5
  //
6
6
 
7
7
  import { next as A } from '@automerge/automerge';
8
- import { StateField, type Extension } from '@codemirror/state';
8
+ import { type Extension, StateField } from '@codemirror/state';
9
9
  import { EditorView, ViewPlugin } from '@codemirror/view';
10
10
 
11
11
  import { type DocAccessor } from '@dxos/react-client/echo';
12
12
 
13
+ import { Cursor } from '../../util';
14
+
13
15
  import { cursorConverter } from './cursor';
14
- import { updateHeadsEffect, isReconcile, type State } from './defs';
16
+ import { type State, isReconcile, updateHeadsEffect } from './defs';
15
17
  import { Syncer } from './sync';
16
- import { Cursor } from '../../util';
17
18
 
18
19
  export const automerge = (accessor: DocAccessor): Extension => {
19
20
  const syncState = StateField.define<State>({
@@ -5,7 +5,7 @@
5
5
  //
6
6
 
7
7
  import { type Heads, type Prop } from '@automerge/automerge';
8
- import { Annotation, StateEffect, type StateField, type EditorState, type Transaction } from '@codemirror/state';
8
+ import { Annotation, type EditorState, StateEffect, type StateField, type Transaction } from '@codemirror/state';
9
9
 
10
10
  export type State = {
11
11
  path: Prop[];
@@ -10,7 +10,7 @@ import { type EditorView } from '@codemirror/view';
10
10
 
11
11
  import { type IDocHandle } from '@dxos/react-client/echo';
12
12
 
13
- import { getLastHeads, getPath, isReconcile, reconcileAnnotation, type State, updateHeads } from './defs';
13
+ import { type State, getLastHeads, getPath, isReconcile, reconcileAnnotation, updateHeads } from './defs';
14
14
  import { updateAutomerge } from './update-automerge';
15
15
  import { updateCodeMirror } from './update-codemirror';
16
16
 
@@ -5,7 +5,7 @@
5
5
  //
6
6
 
7
7
  import { next as A, type Heads } from '@automerge/automerge';
8
- import { type EditorState, type StateField, type Transaction, type Text } from '@codemirror/state';
8
+ import { type EditorState, type StateField, type Text, type Transaction } from '@codemirror/state';
9
9
 
10
10
  import { type IDocHandle } from '@dxos/react-client/echo';
11
11
 
@@ -0,0 +1,157 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { StateEffect } from '@codemirror/state';
6
+ import { EditorView, ViewPlugin } from '@codemirror/view';
7
+
8
+ import { Domino } from '@dxos/react-ui';
9
+
10
+ const lineHeight = 24;
11
+
12
+ export const scrollToBottomEffect = StateEffect.define<any>();
13
+
14
+ export type AutoScrollOptions = {
15
+ overscroll: number;
16
+ throttle: number;
17
+ };
18
+
19
+ /**
20
+ * Extension that supports pinning the scroll position and automatically scrolls to the bottom when content is added.
21
+ */
22
+ // TODO(burdon): Reconcile with transcript-extension.
23
+ export const autoScroll = ({ overscroll = 4 * lineHeight, throttle = 2_000 }: Partial<AutoScrollOptions> = {}) => {
24
+ let isThrottled = false;
25
+ let isPinned = true;
26
+ let timeout: NodeJS.Timeout | undefined;
27
+ let buttonContainer: HTMLDivElement | undefined;
28
+ let lastScrollTop = 0;
29
+ let scrollCounter = 0;
30
+
31
+ const hideScrollbar = (view: EditorView) => {
32
+ view.scrollDOM.classList.add('cm-hide-scrollbar');
33
+ clearTimeout(timeout);
34
+ timeout = setTimeout(() => {
35
+ view.scrollDOM.classList.remove('cm-hide-scrollbar');
36
+ }, 1_000);
37
+ };
38
+
39
+ const scrollToBottom = (view: EditorView) => {
40
+ isPinned = true;
41
+ scrollCounter = 0;
42
+ buttonContainer?.classList.add('opacity-0');
43
+ requestAnimationFrame(() => {
44
+ hideScrollbar(view);
45
+ view.scrollDOM.scrollTo({
46
+ top: view.scrollDOM.scrollHeight,
47
+ behavior: 'smooth',
48
+ });
49
+ });
50
+ };
51
+
52
+ return [
53
+ // Update listener for logging when scrolling is needed.
54
+ EditorView.updateListener.of((update) => {
55
+ // Listen for effects.
56
+ update.transactions.forEach((transaction) => {
57
+ for (const effect of transaction.effects) {
58
+ if (effect.is(scrollToBottomEffect)) {
59
+ scrollToBottom(update.view);
60
+ }
61
+ }
62
+ });
63
+
64
+ // Maybe scroll if doc changed and pinned.
65
+ if (update.docChanged && isPinned && !isThrottled) {
66
+ const distanceFromBottom = calcDistance(update.view.scrollDOM);
67
+ if (distanceFromBottom >= overscroll) {
68
+ isThrottled = true;
69
+ requestAnimationFrame(() => {
70
+ scrollToBottom(update.view);
71
+ });
72
+
73
+ // Reset throttle.
74
+ setTimeout(() => {
75
+ isThrottled = false;
76
+ scrollToBottom(update.view);
77
+ }, throttle);
78
+ }
79
+ }
80
+ }),
81
+
82
+ // Detect user scroll.
83
+ // NOTE: Multiple scroll events are triggered during programmatic smooth scrolling.
84
+ EditorView.domEventHandlers({
85
+ scroll: (event, view) => {
86
+ const scroller = view.scrollDOM;
87
+ // Suspect delta goes positive when rendering widgets, so count positive deltas.
88
+ // TODO(burdon): Detect user scroll directly (wheel, touch, keys, etc.)
89
+ if (lastScrollTop > scroller.scrollTop) {
90
+ scrollCounter++;
91
+ }
92
+ lastScrollTop = scroller.scrollTop;
93
+ const distanceFromBottom = calcDistance(scroller);
94
+ if (distanceFromBottom === 0) {
95
+ // Pin to bottom.
96
+ isPinned = true;
97
+ buttonContainer?.classList.add('opacity-0');
98
+ scrollCounter = 0;
99
+ } else if (scrollCounter > 3) {
100
+ // Break pin if user scrolls up.
101
+ isPinned = false;
102
+ buttonContainer?.classList.remove('opacity-0');
103
+ }
104
+ },
105
+ }),
106
+
107
+ // Scroll button.
108
+ ViewPlugin.fromClass(
109
+ class {
110
+ constructor(view: EditorView) {
111
+ const scroller = view.scrollDOM.parentElement;
112
+ buttonContainer = Domino.of('div')
113
+ .classNames(true && 'cm-scroll-button transition-opacity duration-300 opacity-0')
114
+ .children(
115
+ Domino.of('button')
116
+ .classNames('dx-button bg-accentSurface')
117
+ .data('density', 'fine')
118
+ .children(Domino.of<any>('dx-icon').attr('icon', 'ph--arrow-down--regular'))
119
+ .on('click', () => {
120
+ scrollToBottom(view);
121
+ }),
122
+ )
123
+ .build();
124
+ scroller?.appendChild(buttonContainer);
125
+ }
126
+ },
127
+ ),
128
+
129
+ // Styles.
130
+ EditorView.theme({
131
+ '.cm-scroller': {
132
+ paddingBottom: `${overscroll}px`,
133
+ scrollbarWidth: 'thin',
134
+ },
135
+ '.cm-scroller.cm-hide-scrollbar': {
136
+ scrollbarWidth: 'none',
137
+ },
138
+ '.cm-scroller.cm-hide-scrollbar::-webkit-scrollbar': {
139
+ display: 'none',
140
+ },
141
+
142
+ '.cm-scroll-button': {
143
+ position: 'absolute',
144
+ bottom: '0.5rem',
145
+ right: '1rem',
146
+ },
147
+ }),
148
+ ];
149
+ };
150
+
151
+ const calcDistance = (scroller: HTMLElement) => {
152
+ const scrollTop = scroller.scrollTop;
153
+ const scrollHeight = scroller.scrollHeight;
154
+ const clientHeight = scroller.clientHeight;
155
+ const distanceFromBottom = scrollHeight - (scrollTop + clientHeight);
156
+ return distanceFromBottom;
157
+ };
@@ -2,7 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { Annotation, type Extension, RangeSet, type Range } from '@codemirror/state';
5
+ import { Annotation, type Extension, type Range, RangeSet } from '@codemirror/state';
6
6
  import {
7
7
  Decoration,
8
8
  type DecorationSet,
@@ -16,7 +16,7 @@ import {
16
16
  import { Event } from '@dxos/async';
17
17
  import { Context } from '@dxos/context';
18
18
 
19
- import { singleValueFacet, Cursor, type CursorConverter } from '../../util';
19
+ import { Cursor, type CursorConverter, singleValueFacet } from '../../util';
20
20
 
21
21
  export interface AwarenessProvider {
22
22
  remoteStateChange: Event<void>;
@@ -3,7 +3,7 @@
3
3
  //
4
4
 
5
5
  import { StateEffect } from '@codemirror/state';
6
- import { type KeyBinding, type Command, type EditorView } from '@codemirror/view';
6
+ import { type Command, type EditorView, type KeyBinding } from '@codemirror/view';
7
7
 
8
8
  import { commandState } from './state';
9
9
 
@@ -45,8 +45,7 @@ export const closeCommand: Command = (view: EditorView) => {
45
45
 
46
46
  export const commandKeyBindings: readonly KeyBinding[] = [
47
47
  {
48
- key: '/',
49
- preventDefault: true,
48
+ key: '?',
50
49
  run: openCommand,
51
50
  },
52
51
  {
@@ -2,14 +2,14 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { Prec, type Extension } from '@codemirror/state';
5
+ import { type Extension, Prec } from '@codemirror/state';
6
6
  import { EditorView, keymap } from '@codemirror/view';
7
7
 
8
8
  import { isNonNullable } from '@dxos/util';
9
9
 
10
10
  import { closeEffect, commandKeyBindings } from './action';
11
- import { hint, type HintOptions } from './hint';
12
- import { commandConfig, commandState, type PopupOptions } from './state';
11
+ import { type HintOptions, hint } from './hint';
12
+ import { type PopupOptions, commandConfig, commandState } from './state';
13
13
 
14
14
  // TODO(burdon): Create knowledge base for CM notes and ideas.
15
15
  // https://discuss.codemirror.net/t/inline-code-hints-like-vscode/5533/4
@@ -18,7 +18,7 @@ import { commandConfig, commandState, type PopupOptions } from './state';
18
18
 
19
19
  export type CommandOptions = Partial<PopupOptions & HintOptions>;
20
20
 
21
- export const command = (options: CommandOptions = {}): Extension => {
21
+ export const commandDialog = (options: CommandOptions = {}): Extension => {
22
22
  return [
23
23
  Prec.highest(keymap.of(commandKeyBindings)),
24
24
  commandConfig.of(options),
@@ -6,9 +6,10 @@
6
6
  import { RangeSetBuilder } from '@codemirror/state';
7
7
  import { Decoration, EditorView, ViewPlugin, type ViewUpdate, WidgetType } from '@codemirror/view';
8
8
 
9
- import { commandState } from './state';
10
9
  import { clientRectsFor, flattenRect } from '../../util';
11
10
 
11
+ import { commandState } from './state';
12
+
12
13
  export type HintOptions = {
13
14
  delay?: number;
14
15
  onHint?: () => string | undefined;
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './command-dialog';
6
+ export { type Action } from './action';
7
+ export * from './typeahead';
@@ -3,13 +3,14 @@
3
3
  //
4
4
 
5
5
  import { StateField } from '@codemirror/state';
6
- import { showTooltip, type EditorView, type Tooltip, type TooltipView } from '@codemirror/view';
6
+ import { type EditorView, type Tooltip, type TooltipView, showTooltip } from '@codemirror/view';
7
7
 
8
- import { closeEffect, type Action, openEffect } from './action';
9
- import { type CommandOptions } from './command';
10
8
  import { type RenderCallback } from '../../types';
11
9
  import { singleValueFacet } from '../../util';
12
10
 
11
+ import { type Action, closeEffect, openEffect } from './action';
12
+ import { type CommandOptions } from './command-dialog';
13
+
13
14
  export const commandConfig = singleValueFacet<CommandOptions>();
14
15
 
15
16
  export type PopupOptions = {
@@ -2,15 +2,15 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { EditorSelection, Prec, RangeSetBuilder, type Extension } from '@codemirror/state';
5
+ import { EditorSelection, type Extension, Prec, RangeSetBuilder } from '@codemirror/state';
6
6
  import {
7
7
  type Command,
8
8
  Decoration,
9
9
  type DecorationSet,
10
10
  type EditorView,
11
- keymap,
12
11
  ViewPlugin,
13
12
  type ViewUpdate,
13
+ keymap,
14
14
  } from '@codemirror/view';
15
15
 
16
16
  import { Hint } from './hint';
@@ -2,12 +2,13 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { RangeSetBuilder, StateField, StateEffect, Prec } from '@codemirror/state';
6
- import { EditorView, ViewPlugin, type ViewUpdate, Decoration, keymap, type DecorationSet } from '@codemirror/view';
5
+ import { type Extension, Prec, RangeSetBuilder, StateEffect, StateField } from '@codemirror/state';
6
+ import { Decoration, type DecorationSet, EditorView, ViewPlugin, type ViewUpdate, keymap } from '@codemirror/view';
7
7
 
8
- import { placeholder, type PlaceholderOptions } from './placeholder';
9
8
  import { type Range } from '../../types';
10
9
 
10
+ import { type PlaceholderOptions, placeholder } from './placeholder';
11
+
11
12
  export type CommandMenuOptions = {
12
13
  trigger: string | string[];
13
14
  placeholder?: Partial<PlaceholderOptions>;
@@ -21,7 +22,7 @@ export type CommandMenuOptions = {
21
22
  onTextChange?: (trigger: string, text: string) => void;
22
23
  };
23
24
 
24
- export const commandMenu = (options: CommandMenuOptions) => {
25
+ export const commandMenu = (options: CommandMenuOptions): Extension => {
25
26
  const commandMenuPlugin = ViewPlugin.fromClass(
26
27
  class {
27
28
  decorations: DecorationSet = Decoration.none;
@@ -37,14 +38,15 @@ export const commandMenu = (options: CommandMenuOptions) => {
37
38
  // Check if we should show the widget - only if cursor is within the active command range.
38
39
  const shouldShowWidget = activeRange && selection.head >= activeRange.from && selection.head <= activeRange.to;
39
40
  if (shouldShowWidget) {
40
- // Create mark decoration that wraps the entire line content in a dx-ref-tag.
41
+ // Create mark decoration that wraps the entire line content in a dx-anchor.
41
42
  builder.add(
42
43
  activeRange.from,
43
44
  activeRange.to,
44
45
  Decoration.mark({
45
- tagName: 'dx-ref-tag',
46
- class: 'cm-ref-tag',
46
+ tagName: 'dx-anchor',
47
+ class: 'cm-floating-menu-trigger',
47
48
  attributes: {
49
+ 'data-visible-focus': 'false',
48
50
  'data-auto-trigger': 'true',
49
51
  'data-trigger': trigger!,
50
52
  },
@@ -76,11 +78,9 @@ export const commandMenu = (options: CommandMenuOptions) => {
76
78
  const commandKeymap = keymap.of([
77
79
  ...triggers.map((trigger) => ({
78
80
  key: trigger,
79
- preventDefault: true,
80
81
  run: (view: EditorView) => {
81
82
  const selection = view.state.selection.main;
82
83
  const line = view.state.doc.lineAt(selection.head);
83
-
84
84
  // Check if we should trigger the command menu:
85
85
  // 1. Empty lines or at the beginning of a line
86
86
  // 2. When there's a preceding space
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './command-menu';
6
+ export * from './useCommandMenu';
@@ -4,7 +4,7 @@
4
4
  //
5
5
 
6
6
  import { type Extension } from '@codemirror/state';
7
- import { Decoration, EditorView, WidgetType, ViewPlugin, type ViewUpdate } from '@codemirror/view';
7
+ import { Decoration, EditorView, ViewPlugin, type ViewUpdate, WidgetType } from '@codemirror/view';
8
8
 
9
9
  import { clientRectsFor, flattenRect } from '../../util';
10
10
 
@@ -2,25 +2,37 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ import { type Extension } from '@codemirror/state';
5
6
  import { type EditorView } from '@codemirror/view';
6
7
  import { type RefObject, useCallback, useMemo, useRef, useState } from 'react';
7
8
 
8
- import { type DxRefTag, type DxRefTagActivate } from '@dxos/lit-ui';
9
+ import { type DxAnchorActivate } from '@dxos/react-ui';
9
10
  import { type MaybePromise } from '@dxos/util';
10
11
 
12
+ import { type CommandMenuGroup, type CommandMenuItem, getItem, getNextItem, getPreviousItem } from '../../components';
13
+
11
14
  import { commandMenu, commandRangeEffect } from './command-menu';
12
15
  import { type PlaceholderOptions } from './placeholder';
13
- import { getItem, getNextItem, getPreviousItem, type CommandMenuGroup, type CommandMenuItem } from '../../components';
14
16
 
15
17
  export type UseCommandMenuOptions = {
16
- viewRef: RefObject<EditorView | undefined>;
18
+ // TODO(burdon): Extensions should not depend directly on the editor view.
19
+ viewRef: RefObject<EditorView | null>;
17
20
  trigger: string | string[];
18
21
  placeholder?: Partial<PlaceholderOptions>;
19
22
  getMenu: (trigger: string, query?: string) => MaybePromise<CommandMenuGroup[]>;
20
23
  };
21
24
 
22
- export const useCommandMenu = ({ viewRef, trigger, placeholder, getMenu }: UseCommandMenuOptions) => {
23
- const triggerRef = useRef<DxRefTag | null>(null);
25
+ export type UseCommandMenu = {
26
+ groupsRef: RefObject<CommandMenuGroup[]>;
27
+ commandMenu: Extension;
28
+ currentItem: string | undefined;
29
+ open: boolean;
30
+ onOpenChange: (open: boolean) => void;
31
+ onActivate: (event: DxAnchorActivate) => void;
32
+ onSelect: (item: CommandMenuItem) => void;
33
+ };
34
+
35
+ export const useCommandMenu = ({ viewRef, trigger, placeholder, getMenu }: UseCommandMenuOptions): UseCommandMenu => {
24
36
  const currentRef = useRef<CommandMenuItem | null>(null);
25
37
  const groupsRef = useRef<CommandMenuGroup[]>([]);
26
38
  const [currentItem, setCurrentItem] = useState<string>();
@@ -32,9 +44,9 @@ export const useCommandMenu = ({ viewRef, trigger, placeholder, getMenu }: UseCo
32
44
  if (open && trigger) {
33
45
  groupsRef.current = await getMenu(trigger);
34
46
  }
47
+
35
48
  setOpen(open);
36
49
  if (!open) {
37
- triggerRef.current = null;
38
50
  setCurrentItem(undefined);
39
51
  viewRef.current?.dispatch({ effects: [commandRangeEffect.of(null)] });
40
52
  }
@@ -42,14 +54,13 @@ export const useCommandMenu = ({ viewRef, trigger, placeholder, getMenu }: UseCo
42
54
  [getMenu],
43
55
  );
44
56
 
45
- const handleActivate = useCallback(
46
- async (event: DxRefTagActivate) => {
57
+ const handleActivate = useCallback<UseCommandMenu['onActivate']>(
58
+ async (event) => {
47
59
  const item = getItem(groupsRef.current, currentItem);
48
60
  if (item) {
49
61
  currentRef.current = item;
50
62
  }
51
63
 
52
- triggerRef.current = event.trigger;
53
64
  const triggerKey = event.trigger.getAttribute('data-trigger');
54
65
  if (!open && triggerKey) {
55
66
  await handleOpenChange(true, triggerKey);
@@ -58,18 +69,19 @@ export const useCommandMenu = ({ viewRef, trigger, placeholder, getMenu }: UseCo
58
69
  [open, handleOpenChange],
59
70
  );
60
71
 
61
- const handleSelect = useCallback((item: CommandMenuItem) => {
62
- const view = viewRef.current;
63
- if (!view) {
72
+ // TODO(burdon): Move outside.
73
+ const handleSelect = useCallback<UseCommandMenu['onSelect']>((item) => {
74
+ if (!viewRef.current) {
64
75
  return;
65
76
  }
66
77
 
67
- const selection = view.state.selection.main;
68
- void item.onSelect?.(view, selection.head);
78
+ const selection = viewRef.current.state.selection.main;
79
+ void item.onSelect?.(viewRef.current, selection.head);
69
80
  }, []);
70
81
 
71
82
  const serializedTrigger = Array.isArray(trigger) ? trigger.join(',') : trigger;
72
- const _commandMenu = useMemo(() => {
83
+
84
+ const memoizedCommandMenu = useMemo<Extension>(() => {
73
85
  return commandMenu({
74
86
  trigger,
75
87
  placeholder,
@@ -94,25 +106,29 @@ export const useCommandMenu = ({ viewRef, trigger, placeholder, getMenu }: UseCo
94
106
  }
95
107
  },
96
108
  onTextChange: async (trigger, text) => {
109
+ if (/\W/.test(text)) {
110
+ return queueMicrotask(() => handleOpenChange(false));
111
+ }
112
+
97
113
  groupsRef.current = await getMenu(trigger, text);
98
114
  const firstItem = groupsRef.current.filter((group) => group.items.length > 0)[0]?.items[0];
99
115
  if (firstItem) {
100
116
  setCurrentItem(firstItem.id);
101
117
  currentRef.current = firstItem;
102
118
  }
119
+
103
120
  refresh({});
104
121
  },
105
122
  });
106
123
  }, [handleOpenChange, getMenu, serializedTrigger, placeholder]);
107
124
 
108
125
  return {
109
- commandMenu: _commandMenu,
110
- currentItem,
111
126
  groupsRef,
112
- ref: triggerRef,
127
+ commandMenu: memoizedCommandMenu,
128
+ currentItem,
113
129
  open,
114
- onActivate: handleActivate,
115
130
  onOpenChange: setOpen,
131
+ onActivate: handleActivate,
116
132
  onSelect: handleSelect,
117
133
  };
118
134
  };
@@ -5,25 +5,26 @@
5
5
  import { invertedEffects } from '@codemirror/commands';
6
6
  import { type ChangeDesc, type Extension, StateEffect, StateField, type Text } from '@codemirror/state';
7
7
  import {
8
- hoverTooltip,
9
- keymap,
10
8
  type Command,
11
9
  Decoration,
12
10
  EditorView,
13
- type Rect,
14
11
  type PluginValue,
12
+ type Rect,
15
13
  ViewPlugin,
14
+ hoverTooltip,
15
+ keymap,
16
16
  } from '@codemirror/view';
17
17
  import sortBy from 'lodash.sortby';
18
18
  import { useEffect } from 'react';
19
19
 
20
- import { debounce, type CleanupFn } from '@dxos/async';
20
+ import { type CleanupFn, debounce } from '@dxos/async';
21
21
  import { log } from '@dxos/log';
22
22
  import { isNonNullable } from '@dxos/util';
23
23
 
24
+ import { type Comment, type Range, type RenderCallback } from '../types';
25
+ import { Cursor, callbackWrapper, singleValueFacet } from '../util';
26
+
24
27
  import { documentId } from './selection';
25
- import { type RenderCallback, type Comment, type Range } from '../types';
26
- import { Cursor, singleValueFacet, callbackWrapper } from '../util';
27
28
 
28
29
  //
29
30
  // State management.
@@ -57,7 +58,11 @@ const setCommentState = StateEffect.define<CommentsState>();
57
58
  * The ranges are tracked as Automerge cursors from which the absolute indexed ranges can be computed.
58
59
  */
59
60
  export const commentsState = StateField.define<CommentsState>({
60
- create: (state) => ({ id: state.facet(documentId), comments: [], selection: {} }),
61
+ create: (state) => ({
62
+ id: state.facet(documentId),
63
+ comments: [],
64
+ selection: {},
65
+ }),
61
66
  update: (value, tr) => {
62
67
  for (const effect of tr.effects) {
63
68
  // Update selection.
@@ -98,16 +103,16 @@ export const commentsState = StateField.define<CommentsState>({
98
103
  */
99
104
  const styles = EditorView.theme({
100
105
  '.cm-comment, .cm-comment-current': {
101
- margin: '0 -3px',
102
- padding: '3px',
103
- borderRadius: '3px',
106
+ padding: '3px 0',
107
+ backgroundColor: 'var(--dx-cmCommentSurface)',
108
+ },
109
+ '.cm-comment > span, .cm-comment-current > span': {
110
+ boxDecorationBreak: 'clone',
111
+ boxShadow: '0 0 1px 3px var(--dx-cmCommentSurface)',
104
112
  backgroundColor: 'var(--dx-cmCommentSurface)',
105
113
  color: 'var(--dx-cmComment)',
106
114
  cursor: 'pointer',
107
115
  },
108
- '.cm-comment:hover, .cm-comment-current': {
109
- textDecoration: 'underline',
110
- },
111
116
  });
112
117
 
113
118
  const createCommentMark = (id: string, isCurrent: boolean) =>
@@ -3,7 +3,7 @@
3
3
  //
4
4
 
5
5
  import type { Extension } from '@codemirror/state';
6
- import { dropCursor, EditorView } from '@codemirror/view';
6
+ import { EditorView, dropCursor } from '@codemirror/view';
7
7
 
8
8
  export type DNDOptions = { onDrop?: (view: EditorView, event: { files: FileList }) => void };
9
9